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}