icu_locale_core/extensions/transform/
mod.rs1mod fields;
34mod key;
35mod value;
36
37use core::cmp::Ordering;
38#[cfg(feature = "alloc")]
39use core::str::FromStr;
40
41pub use fields::Fields;
42#[doc(inline)]
43pub use key::{key, Key};
44pub use value::Value;
45
46#[cfg(feature = "alloc")]
47use super::ExtensionType;
48#[cfg(feature = "alloc")]
49use crate::parser::SubtagIterator;
50#[cfg(feature = "alloc")]
51use crate::parser::{parse_language_identifier_from_iter, ParseError, ParserMode};
52#[cfg(feature = "alloc")]
53use crate::shortvec::ShortBoxSlice;
54use crate::subtags;
55#[cfg(feature = "alloc")]
56use crate::subtags::Language;
57use crate::LanguageIdentifier;
58#[cfg(feature = "alloc")]
59use litemap::LiteMap;
60
61pub(crate) const TRANSFORM_EXT_CHAR: char = 't';
62pub(crate) const TRANSFORM_EXT_STR: &str = "t";
63
64#[derive(Clone, PartialEq, Eq, Debug, Default, Hash)]
91#[allow(clippy::exhaustive_structs)] pub struct Transform {
93    pub lang: Option<LanguageIdentifier>,
95    pub fields: Fields,
98}
99
100impl Transform {
101    #[inline]
111    pub const fn new() -> Self {
112        Self {
113            lang: None,
114            fields: Fields::new(),
115        }
116    }
117
118    #[inline]
121    #[cfg(feature = "alloc")]
122    pub fn try_from_str(s: &str) -> Result<Self, ParseError> {
123        Self::try_from_utf8(s.as_bytes())
124    }
125
126    #[cfg(feature = "alloc")]
128    pub fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
129        let mut iter = SubtagIterator::new(code_units);
130
131        let ext = iter.next().ok_or(ParseError::InvalidExtension)?;
132        if let ExtensionType::Transform = ExtensionType::try_from_byte_slice(ext)? {
133            return Self::try_from_iter(&mut iter);
134        }
135
136        Err(ParseError::InvalidExtension)
137    }
138
139    pub fn is_empty(&self) -> bool {
151        self.lang.is_none() && self.fields.is_empty()
152    }
153
154    pub fn clear(&mut self) {
166        self.lang = None;
167        self.fields.clear();
168    }
169
170    #[allow(clippy::type_complexity)]
171    pub(crate) fn as_tuple(
172        &self,
173    ) -> (
174        Option<(
175            subtags::Language,
176            Option<subtags::Script>,
177            Option<subtags::Region>,
178            &subtags::Variants,
179        )>,
180        &Fields,
181    ) {
182        (self.lang.as_ref().map(|l| l.as_tuple()), &self.fields)
183    }
184
185    pub fn total_cmp(&self, other: &Self) -> Ordering {
192        self.as_tuple().cmp(&other.as_tuple())
193    }
194
195    #[cfg(feature = "alloc")]
196    pub(crate) fn try_from_iter(iter: &mut SubtagIterator) -> Result<Self, ParseError> {
197        let mut tlang = None;
198        let mut tfields = LiteMap::new();
199
200        if let Some(subtag) = iter.peek() {
201            if Language::try_from_utf8(subtag).is_ok() {
202                tlang = Some(parse_language_identifier_from_iter(
203                    iter,
204                    ParserMode::Partial,
205                )?);
206            }
207        }
208
209        let mut current_tkey = None;
210        let mut current_tvalue = ShortBoxSlice::new();
211        let mut has_current_tvalue = false;
212
213        while let Some(subtag) = iter.peek() {
214            if let Some(tkey) = current_tkey {
215                if let Ok(val) = Value::parse_subtag(subtag) {
216                    has_current_tvalue = true;
217                    if let Some(val) = val {
218                        current_tvalue.push(val);
219                    }
220                } else {
221                    if !has_current_tvalue {
222                        return Err(ParseError::InvalidExtension);
223                    }
224                    tfields.try_insert(tkey, Value::from_short_slice_unchecked(current_tvalue));
225                    current_tkey = None;
226                    current_tvalue = ShortBoxSlice::new();
227                    has_current_tvalue = false;
228                    continue;
229                }
230            } else if let Ok(tkey) = Key::try_from_utf8(subtag) {
231                current_tkey = Some(tkey);
232            } else {
233                break;
234            }
235
236            iter.next();
237        }
238
239        if let Some(tkey) = current_tkey {
240            if !has_current_tvalue {
241                return Err(ParseError::InvalidExtension);
242            }
243            tfields.try_insert(tkey, Value::from_short_slice_unchecked(current_tvalue));
244        }
245
246        if tlang.is_none() && tfields.is_empty() {
247            Err(ParseError::InvalidExtension)
248        } else {
249            Ok(Self {
250                lang: tlang,
251                fields: tfields.into(),
252            })
253        }
254    }
255
256    pub(crate) fn for_each_subtag_str<E, F>(&self, f: &mut F, with_ext: bool) -> Result<(), E>
257    where
258        F: FnMut(&str) -> Result<(), E>,
259    {
260        if self.is_empty() {
261            return Ok(());
262        }
263        if with_ext {
264            f(TRANSFORM_EXT_STR)?;
265        }
266        if let Some(lang) = &self.lang {
267            lang.for_each_subtag_str_lowercased(f)?;
268        }
269        self.fields.for_each_subtag_str(f)
270    }
271}
272
273#[cfg(feature = "alloc")]
274impl FromStr for Transform {
275    type Err = ParseError;
276
277    #[inline]
278    fn from_str(s: &str) -> Result<Self, Self::Err> {
279        Self::try_from_str(s)
280    }
281}
282
283writeable::impl_display_with_writeable!(Transform);
284
285impl writeable::Writeable for Transform {
286    fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result {
287        if self.is_empty() {
288            return Ok(());
289        }
290        sink.write_char(TRANSFORM_EXT_CHAR)?;
291        if let Some(lang) = &self.lang {
292            sink.write_char('-')?;
293            lang.write_lowercased_to(sink)?;
294        }
295        if !self.fields.is_empty() {
296            sink.write_char('-')?;
297            writeable::Writeable::write_to(&self.fields, sink)?;
298        }
299        Ok(())
300    }
301
302    fn writeable_length_hint(&self) -> writeable::LengthHint {
303        if self.is_empty() {
304            return writeable::LengthHint::exact(0);
305        }
306        let mut result = writeable::LengthHint::exact(1);
307        if let Some(lang) = &self.lang {
308            result += writeable::Writeable::writeable_length_hint(lang) + 1;
309        }
310        if !self.fields.is_empty() {
311            result += writeable::Writeable::writeable_length_hint(&self.fields) + 1;
312        }
313        result
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_transform_extension_fromstr() {
323        let te: Transform = "t-en-us-h0-hybrid"
324            .parse()
325            .expect("Failed to parse Transform");
326        assert_eq!(te.to_string(), "t-en-us-h0-hybrid");
327
328        let te: Result<Transform, _> = "t".parse();
329        assert!(te.is_err());
330    }
331}