icu_locale_core/extensions/other/
mod.rs

1// This file is part of ICU4X. For terms of use, please see the file
2// called LICENSE at the top level of the ICU4X source tree
3// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4
5//! Other Use Extensions is a list of extensions other than unicode,
6//! transform or private.
7//!
8//! Those extensions are treated as a pass-through, and no Unicode related
9//! behavior depends on them.
10//!
11//! The main struct for this extension is [`Other`] which is a list of [`Subtag`]s.
12//!
13//! # Examples
14//!
15//! ```
16//! use icu::locale::extensions::other::Other;
17//! use icu::locale::Locale;
18//!
19//! let mut loc: Locale = "en-US-a-foo-faa".parse().expect("Parsing failed.");
20//! ```
21
22#[cfg(feature = "alloc")]
23use core::str::FromStr;
24
25#[cfg(feature = "alloc")]
26use super::ExtensionType;
27#[cfg(feature = "alloc")]
28use crate::parser::ParseError;
29#[cfg(feature = "alloc")]
30use crate::parser::SubtagIterator;
31use crate::shortvec::ShortBoxSlice;
32use crate::subtags::Subtag;
33#[cfg(feature = "alloc")]
34use alloc::vec::Vec;
35
36/// A list of [`Other Use Extensions`] as defined in [`Unicode Locale
37/// Identifier`] specification.
38///
39/// Those extensions are treated as a pass-through, and no Unicode related
40/// behavior depends on them.
41///
42/// # Examples
43///
44/// ```
45/// use icu::locale::extensions::other::Other;
46/// use icu::locale::subtags::Subtag;
47///
48/// let subtag1: Subtag = "foo".parse().expect("Failed to parse a Subtag.");
49/// let subtag2: Subtag = "bar".parse().expect("Failed to parse a Subtag.");
50///
51/// let other = Other::from_vec_unchecked(b'a', vec![subtag1, subtag2]);
52/// assert_eq!(&other.to_string(), "a-foo-bar");
53/// ```
54///
55/// [`Other Use Extensions`]: https://unicode.org/reports/tr35/#other_extensions
56/// [`Unicode Locale Identifier`]: https://unicode.org/reports/tr35/#Unicode_locale_identifier
57#[derive(Clone, PartialEq, Eq, Debug, Default, Hash, PartialOrd, Ord)]
58pub struct Other {
59    // Safety invariant: must be ASCII
60    ext: u8,
61    keys: ShortBoxSlice<Subtag>,
62}
63
64impl Other {
65    /// A constructor which takes a str slice, parses it and
66    /// produces a well-formed [`Other`].
67    #[inline]
68    #[cfg(feature = "alloc")]
69    pub fn try_from_str(s: &str) -> Result<Self, ParseError> {
70        Self::try_from_utf8(s.as_bytes())
71    }
72
73    /// See [`Self::try_from_str`]
74    #[cfg(feature = "alloc")]
75    pub fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
76        let mut iter = SubtagIterator::new(code_units);
77
78        let ext = iter.next().ok_or(ParseError::InvalidExtension)?;
79        if let ExtensionType::Other(b) = ExtensionType::try_from_byte_slice(ext)? {
80            return Self::try_from_iter(b, &mut iter);
81        }
82
83        Err(ParseError::InvalidExtension)
84    }
85
86    /// A constructor which takes a pre-sorted list of [`Subtag`].
87    ///
88    /// # Panics
89    ///
90    /// Panics if `ext` is not ASCII alphabetic.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use icu::locale::extensions::other::Other;
96    /// use icu::locale::subtags::Subtag;
97    ///
98    /// let subtag1: Subtag = "foo".parse().expect("Failed to parse a Subtag.");
99    /// let subtag2: Subtag = "bar".parse().expect("Failed to parse a Subtag.");
100    ///
101    /// let other = Other::from_vec_unchecked(b'a', vec![subtag1, subtag2]);
102    /// assert_eq!(&other.to_string(), "a-foo-bar");
103    /// ```
104    #[cfg(feature = "alloc")]
105    pub fn from_vec_unchecked(ext: u8, keys: Vec<Subtag>) -> Self {
106        Self::from_short_slice_unchecked(ext, keys.into())
107    }
108
109    #[allow(dead_code)]
110    pub(crate) fn from_short_slice_unchecked(ext: u8, keys: ShortBoxSlice<Subtag>) -> Self {
111        assert!(ext.is_ascii_alphabetic());
112        // Safety invariant upheld here: ext checked as ASCII above
113        Self { ext, keys }
114    }
115
116    #[cfg(feature = "alloc")]
117    pub(crate) fn try_from_iter(ext: u8, iter: &mut SubtagIterator) -> Result<Self, ParseError> {
118        debug_assert!(matches!(
119            ExtensionType::try_from_byte(ext),
120            Ok(ExtensionType::Other(_)),
121        ));
122
123        let mut keys = ShortBoxSlice::new();
124        while let Some(subtag) = iter.peek() {
125            if !Subtag::valid_key(subtag) {
126                break;
127            }
128            if let Ok(key) = Subtag::try_from_utf8(subtag) {
129                keys.push(key);
130            }
131            iter.next();
132        }
133
134        if keys.is_empty() {
135            Err(ParseError::InvalidExtension)
136        } else {
137            Ok(Self::from_short_slice_unchecked(ext, keys))
138        }
139    }
140
141    /// Gets the tag character for this extension as a &str.
142    ///
143    /// # Examples
144    ///
145    /// ```
146    /// use icu::locale::Locale;
147    ///
148    /// let loc: Locale = "und-a-hello-world".parse().unwrap();
149    /// let other_ext = &loc.extensions.other[0];
150    /// assert_eq!(other_ext.get_ext_str(), "a");
151    /// ```
152    pub fn get_ext_str(&self) -> &str {
153        debug_assert!(self.ext.is_ascii_alphabetic());
154        // Safety: from safety invariant on self.ext (that it is ASCII)
155        unsafe { core::str::from_utf8_unchecked(core::slice::from_ref(&self.ext)) }
156    }
157
158    /// Gets the tag character for this extension as a char.
159    ///
160    /// # Examples
161    ///
162    /// ```
163    /// use icu::locale::Locale;
164    ///
165    /// let loc: Locale = "und-a-hello-world".parse().unwrap();
166    /// let other_ext = &loc.extensions.other[0];
167    /// assert_eq!(other_ext.get_ext(), 'a');
168    /// ```
169    pub fn get_ext(&self) -> char {
170        self.ext as char
171    }
172
173    /// Gets the tag character for this extension as a byte.
174    ///
175    /// # Examples
176    ///
177    /// ```
178    /// use icu::locale::Locale;
179    ///
180    /// let loc: Locale = "und-a-hello-world".parse().unwrap();
181    /// let other_ext = &loc.extensions.other[0];
182    /// assert_eq!(other_ext.get_ext_byte(), b'a');
183    /// ```
184    pub fn get_ext_byte(&self) -> u8 {
185        self.ext
186    }
187
188    pub(crate) fn for_each_subtag_str<E, F>(&self, f: &mut F, with_ext: bool) -> Result<(), E>
189    where
190        F: FnMut(&str) -> Result<(), E>,
191    {
192        if self.keys.is_empty() {
193            return Ok(());
194        }
195
196        if with_ext {
197            f(self.get_ext_str())?;
198        }
199        self.keys.iter().map(|t| t.as_str()).try_for_each(f)
200    }
201}
202
203#[cfg(feature = "alloc")]
204impl FromStr for Other {
205    type Err = ParseError;
206
207    #[inline]
208    fn from_str(s: &str) -> Result<Self, Self::Err> {
209        Self::try_from_str(s)
210    }
211}
212
213writeable::impl_display_with_writeable!(Other);
214
215impl writeable::Writeable for Other {
216    fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result {
217        if self.keys.is_empty() {
218            return Ok(());
219        }
220        sink.write_str(self.get_ext_str())?;
221        for key in self.keys.iter() {
222            sink.write_char('-')?;
223            writeable::Writeable::write_to(key, sink)?;
224        }
225
226        Ok(())
227    }
228
229    fn writeable_length_hint(&self) -> writeable::LengthHint {
230        if self.keys.is_empty() {
231            return writeable::LengthHint::exact(0);
232        };
233        let mut result = writeable::LengthHint::exact(1);
234        for key in self.keys.iter() {
235            result += writeable::Writeable::writeable_length_hint(key) + 1;
236        }
237        result
238    }
239
240    #[cfg(feature = "alloc")]
241    fn write_to_string(&self) -> alloc::borrow::Cow<str> {
242        if self.keys.is_empty() {
243            return alloc::borrow::Cow::Borrowed("");
244        }
245        let mut string =
246            alloc::string::String::with_capacity(self.writeable_length_hint().capacity());
247        let _ = self.write_to(&mut string);
248        alloc::borrow::Cow::Owned(string)
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn test_other_extension_fromstr() {
258        let oe: Other = "o-foo-bar".parse().expect("Failed to parse Other");
259        assert_eq!(oe.to_string(), "o-foo-bar");
260
261        let oe: Result<Other, _> = "o".parse();
262        assert!(oe.is_err());
263    }
264}