Skip to main content

jsonld_core/
lang_string.rs

1use crate::{Direction, LenientLangTag, LenientLangTagBuf, object::InvalidExpandedJson};
2
3/// Language string.
4///
5/// A language string is a string tagged with language and reading direction information.
6///
7/// A valid language string is associated to either a language tag or a direction, or both.
8#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize))]
10pub struct LangString {
11    /// Actual content of the string.
12    #[cfg_attr(feature = "serde", serde(rename = "@value"))]
13    data: jsonld_syntax::String,
14
15    #[cfg_attr(feature = "serde", serde(rename = "@language", skip_serializing_if = "Option::is_none"))]
16    language: Option<LenientLangTagBuf>,
17
18    #[cfg_attr(feature = "serde", serde(rename = "@direction", skip_serializing_if = "Option::is_none"))]
19    direction: Option<Direction>,
20}
21
22/// Raised when something tried to build a language string without language tag or direction.
23///
24/// Carries back the recovered string data so the caller can fall back to a
25/// non-language string.
26#[derive(Clone, Debug, thiserror::Error)]
27#[error("language string requires a language tag or direction")]
28pub struct InvalidLangString(pub jsonld_syntax::String);
29
30/// Raised when a [`LangString`] mutation would leave it without both a language
31/// tag and a direction.
32#[derive(Clone, Copy, Debug, thiserror::Error)]
33#[error("language string requires a language tag or direction")]
34pub struct MissingLangQualifier;
35
36impl LangString {
37    /// Create a new language string.
38    ///
39    /// # Errors
40    ///
41    /// Returns an error when the input is not valid for this type.
42    pub fn new(data: jsonld_syntax::String, language: Option<LenientLangTagBuf>, direction: Option<Direction>) -> Result<Self, InvalidLangString> {
43        if language.is_some() || direction.is_some() {
44            Ok(Self { data, language, direction })
45        } else {
46            Err(InvalidLangString(data))
47        }
48    }
49
50    /// Total constructor: build a language string with a known language tag.
51    #[inline]
52    #[must_use]
53    pub fn with_language(data: jsonld_syntax::String, language: LenientLangTagBuf) -> Self {
54        Self {
55            data,
56            language: Some(language),
57            direction: None,
58        }
59    }
60
61    /// Total constructor: build a language string with a known direction.
62    #[inline]
63    #[must_use]
64    pub fn with_direction(data: jsonld_syntax::String, direction: Direction) -> Self {
65        Self {
66            data,
67            language: None,
68            direction: Some(direction),
69        }
70    }
71
72    /// Total constructor: build a language string with both a language tag and a direction.
73    #[inline]
74    #[must_use]
75    pub fn with_language_and_direction(data: jsonld_syntax::String, language: LenientLangTagBuf, direction: Direction) -> Self {
76        Self {
77            data,
78            language: Some(language),
79            direction: Some(direction),
80        }
81    }
82
83    /// Consumes the language string, returning its string data, language tag
84    /// and base direction.
85    #[must_use]
86    pub fn into_parts(self) -> (jsonld_syntax::String, Option<LenientLangTagBuf>, Option<Direction>) {
87        (self.data, self.language, self.direction)
88    }
89
90    /// Returns the string data, language tag and base direction.
91    #[must_use]
92    pub fn parts(&self) -> (&str, Option<&LenientLangTagBuf>, Option<&Direction>) {
93        (&self.data, self.language.as_ref(), self.direction.as_ref())
94    }
95
96    /// Reference to the underlying `str`.
97    #[inline(always)]
98    #[must_use]
99    pub fn as_str(&self) -> &str {
100        self.data.as_ref()
101    }
102
103    /// Gets the associated language tag, if any.
104    #[inline(always)]
105    #[must_use]
106    pub fn language(&self) -> Option<&LenientLangTag> {
107        self.language.as_ref().map(jsonld_syntax::LenientLangTagBuf::as_lenient_lang_tag_ref)
108    }
109
110    /// Sets the associated language tag.
111    ///
112    /// If `None` is given, the direction must be set,
113    /// otherwise this function will fail with a [`MissingLangQualifier`] error.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error when removing the language would leave a string with a direction but no language, which is not a valid language-tagged string.
118    pub fn set_language(&mut self, language: Option<LenientLangTagBuf>) -> Result<(), MissingLangQualifier> {
119        if self.direction.is_some() || language.is_some() {
120            self.language = language;
121            Ok(())
122        } else {
123            Err(MissingLangQualifier)
124        }
125    }
126
127    /// Gets the associated direction, if any.
128    #[inline(always)]
129    #[must_use]
130    pub fn direction(&self) -> Option<Direction> {
131        self.direction
132    }
133
134    /// Sets the associated direction.
135    ///
136    /// If `None` is given, a language tag must be set,
137    /// otherwise this function will fail with a [`MissingLangQualifier`] error.
138    ///
139    /// # Errors
140    ///
141    /// Returns an error when removing the direction would leave a string with neither language nor direction.
142    pub fn set_direction(&mut self, direction: Option<Direction>) -> Result<(), MissingLangQualifier> {
143        if direction.is_some() || self.language.is_some() {
144            self.direction = direction;
145            Ok(())
146        } else {
147            Err(MissingLangQualifier)
148        }
149    }
150
151    /// Set both the language tag and direction.
152    ///
153    /// If both `language` and `direction` are `None`,
154    /// this function will fail with a [`MissingLangQualifier`] error.
155    ///
156    /// # Errors
157    ///
158    /// Returns an error when neither a language nor a direction is given, which no longer describes a language-tagged string.
159    pub fn set(&mut self, language: Option<LenientLangTagBuf>, direction: Option<Direction>) -> Result<(), MissingLangQualifier> {
160        if direction.is_some() || language.is_some() {
161            self.language = language;
162            self.direction = direction;
163            Ok(())
164        } else {
165            Err(MissingLangQualifier)
166        }
167    }
168
169    /// Returns a reference to this lang string as a [`LangStr`].
170    #[must_use]
171    pub fn as_lang_str(&self) -> LangStr<'_> {
172        LangStr {
173            data: &self.data,
174            language: self.language.as_deref(),
175            direction: self.direction,
176        }
177    }
178
179    pub(crate) fn try_from_json(
180        object: jstrict::Object,
181        value: jstrict::Value,
182        language: Option<jstrict::Value>,
183        direction: Option<jstrict::Value>,
184    ) -> Result<Self, InvalidExpandedJson> {
185        let data = match value {
186            jstrict::Value::String(s) => s,
187            v => return Err(InvalidExpandedJson::Unexpected(v.kind(), jstrict::Kind::String)),
188        };
189
190        let language = match language {
191            Some(jstrict::Value::String(value)) => {
192                let (tag, _) = LenientLangTagBuf::new(value.to_string());
193                Some(tag)
194            }
195            Some(v) => return Err(InvalidExpandedJson::Unexpected(v.kind(), jstrict::Kind::String)),
196            None => None,
197        };
198
199        let direction = match direction {
200            Some(jstrict::Value::String(value)) => match Direction::try_from(value.as_str()) {
201                Ok(direction) => Some(direction),
202                Err(_) => return Err(InvalidExpandedJson::InvalidDirection),
203            },
204            Some(v) => return Err(InvalidExpandedJson::Unexpected(v.kind(), jstrict::Kind::String)),
205            None => None,
206        };
207
208        if object.into_iter().next().is_some() {
209            return Err(InvalidExpandedJson::UnexpectedEntry);
210        }
211
212        match (language, direction) {
213            (Some(lang), Some(dir)) => Ok(Self::with_language_and_direction(data, lang, dir)),
214            (Some(lang), None) => Ok(Self::with_language(data, lang)),
215            (None, Some(dir)) => Ok(Self::with_direction(data, dir)),
216            (None, None) => Err(InvalidExpandedJson::InvalidLangString),
217        }
218    }
219}
220
221#[cfg(feature = "serde")]
222impl<'de> serde::Deserialize<'de> for LangString {
223    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
224    where
225        D: serde::Deserializer<'de>,
226    {
227        #[derive(serde::Deserialize)]
228        struct Value {
229            #[serde(rename = "@value")]
230            data: jsonld_syntax::String,
231            #[serde(rename = "@language")]
232            language: Option<LenientLangTagBuf>,
233            #[serde(rename = "@direction")]
234            direction: Option<Direction>,
235        }
236
237        let value = Value::deserialize(deserializer)?;
238
239        Self::new(value.data, value.language, value.direction).map_err(serde::de::Error::custom)
240    }
241}
242
243/// Language string reference.
244///
245/// A language string is a string tagged with language and reading direction information.
246///
247/// A valid language string is associated to either a language tag or a direction, or both.
248#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
249#[cfg_attr(feature = "serde", derive(serde::Serialize))]
250pub struct LangStr<'a> {
251    /// Actual content of the string.
252    #[cfg_attr(feature = "serde", serde(rename = "@value"))]
253    data: &'a str,
254
255    #[cfg_attr(feature = "serde", serde(rename = "@language", skip_serializing_if = "Option::is_none"))]
256    language: Option<&'a LenientLangTag>,
257
258    #[cfg_attr(feature = "serde", serde(rename = "@direction", skip_serializing_if = "Option::is_none"))]
259    direction: Option<Direction>,
260}
261
262impl<'a> LangStr<'a> {
263    /// Create a new language string reference.
264    ///
265    /// # Errors
266    ///
267    /// Returns an error when the input is not valid for this type.
268    pub fn new(data: &'a str, language: Option<&'a LenientLangTag>, direction: Option<Direction>) -> Result<Self, MissingLangQualifier> {
269        if language.is_some() || direction.is_some() {
270            Ok(Self { data, language, direction })
271        } else {
272            Err(MissingLangQualifier)
273        }
274    }
275
276    /// Consumes the language string, returning its string data, language tag
277    /// and base direction.
278    #[must_use]
279    pub fn into_parts(self) -> (&'a str, Option<&'a LenientLangTag>, Option<Direction>) {
280        (self.data, self.language, self.direction)
281    }
282
283    /// Reference to the underlying `str`.
284    #[inline(always)]
285    #[must_use]
286    pub fn as_str(&self) -> &'a str {
287        self.data
288    }
289
290    /// Gets the associated language tag, if any.
291    #[inline(always)]
292    #[must_use]
293    pub fn language(&self) -> Option<&'a LenientLangTag> {
294        self.language
295    }
296
297    /// Gets the associated direction, if any.
298    #[inline(always)]
299    #[must_use]
300    pub fn direction(&self) -> Option<Direction> {
301        self.direction
302    }
303}