icu_locale_core/extensions/transform/
value.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
5use crate::parser::ParseError;
6#[cfg(feature = "alloc")]
7use crate::parser::SubtagIterator;
8use crate::shortvec::ShortBoxSlice;
9use crate::subtags::{subtag, Subtag};
10use core::ops::RangeInclusive;
11#[cfg(feature = "alloc")]
12use core::str::FromStr;
13
14/// A value used in a list of [`Fields`](super::Fields).
15///
16/// The value has to be a sequence of one or more alphanumerical strings
17/// separated by `-`.
18/// Each part of the sequence has to be no shorter than three characters and no
19/// longer than 8.
20///
21/// # Examples
22///
23/// ```
24/// use icu::locale::extensions::transform::Value;
25///
26/// "hybrid".parse::<Value>().expect("Valid Value.");
27///
28/// "hybrid-foobar".parse::<Value>().expect("Valid Value.");
29///
30/// "no".parse::<Value>().expect_err("Invalid Value.");
31/// ```
32#[derive(Debug, PartialEq, Eq, Clone, Hash, PartialOrd, Ord, Default)]
33pub struct Value(ShortBoxSlice<Subtag>);
34
35#[allow(dead_code)]
36const TYPE_LENGTH: RangeInclusive<usize> = 3..=8;
37const TRUE_TVALUE: Subtag = subtag!("true");
38
39impl Value {
40    /// A constructor which takes a str slice, parses it and
41    /// produces a well-formed [`Value`].
42    ///
43    /// ✨ *Enabled with the `alloc` Cargo feature.*
44    ///
45    /// # Examples
46    ///
47    /// ```
48    /// use icu::locale::extensions::transform::Value;
49    ///
50    /// let value = Value::try_from_str("hybrid").expect("Parsing failed.");
51    /// ```
52    #[inline]
53    #[cfg(feature = "alloc")]
54    pub fn try_from_str(s: &str) -> Result<Self, ParseError> {
55        Self::try_from_utf8(s.as_bytes())
56    }
57
58    /// See [`Self::try_from_str`]
59    ///
60    /// ✨ *Enabled with the `alloc` Cargo feature.*
61    #[cfg(feature = "alloc")]
62    pub fn try_from_utf8(code_units: &[u8]) -> Result<Self, ParseError> {
63        let mut v = ShortBoxSlice::default();
64        let mut has_value = false;
65
66        for subtag in SubtagIterator::new(code_units) {
67            if !Self::is_type_subtag(subtag) {
68                return Err(ParseError::InvalidExtension);
69            }
70            has_value = true;
71            let val = Subtag::try_from_utf8(subtag).map_err(|_| ParseError::InvalidExtension)?;
72            if val != TRUE_TVALUE {
73                v.push(val);
74            }
75        }
76
77        if !has_value {
78            return Err(ParseError::InvalidExtension);
79        }
80        Ok(Self(v))
81    }
82
83    #[allow(dead_code)]
84    pub(crate) fn from_short_slice_unchecked(input: ShortBoxSlice<Subtag>) -> Self {
85        Self(input)
86    }
87
88    #[allow(dead_code)]
89    pub(crate) fn is_type_subtag(t: &[u8]) -> bool {
90        TYPE_LENGTH.contains(&t.len()) && t.iter().all(u8::is_ascii_alphanumeric)
91    }
92
93    #[allow(dead_code)]
94    pub(crate) fn parse_subtag(t: &[u8]) -> Result<Option<Subtag>, ParseError> {
95        if !TYPE_LENGTH.contains(&t.len()) {
96            return Err(ParseError::InvalidExtension);
97        }
98        let s = Subtag::try_from_utf8(t).map_err(|_| ParseError::InvalidSubtag)?;
99
100        let s = s.to_ascii_lowercase();
101
102        if s == TRUE_TVALUE {
103            Ok(None)
104        } else {
105            Ok(Some(s))
106        }
107    }
108
109    pub(crate) fn for_each_subtag_str<E, F>(&self, f: &mut F) -> Result<(), E>
110    where
111        F: FnMut(&str) -> Result<(), E>,
112    {
113        if self.0.is_empty() {
114            f(TRUE_TVALUE.as_str())?;
115        } else {
116            self.0.iter().map(Subtag::as_str).try_for_each(f)?;
117        }
118        Ok(())
119    }
120}
121
122/// ✨ *Enabled with the `alloc` Cargo feature.*
123#[cfg(feature = "alloc")]
124impl FromStr for Value {
125    type Err = ParseError;
126
127    #[inline]
128    fn from_str(s: &str) -> Result<Self, Self::Err> {
129        Self::try_from_str(s)
130    }
131}
132
133impl_writeable_for_each_subtag_str_no_test!(Value, selff, selff.0.is_empty() => Some("true"));
134
135#[test]
136fn test_writeable() {
137    use writeable::assert_writeable_eq;
138
139    let hybrid = "hybrid".parse().unwrap();
140    let foobar = "foobar".parse().unwrap();
141
142    assert_writeable_eq!(Value::default(), "true");
143    assert_writeable_eq!(
144        Value::from_short_slice_unchecked(vec![hybrid].into()),
145        "hybrid"
146    );
147    assert_writeable_eq!(
148        Value::from_short_slice_unchecked(vec![hybrid, foobar].into()),
149        "hybrid-foobar"
150    );
151}
152
153#[test]
154fn test_short_tvalue() {
155    let value = Value::try_from_str("foo-longstag");
156    assert!(value.is_ok());
157    let value = value.unwrap();
158    assert_eq!(value.0.len(), 2);
159    for (s, reference) in value.0.iter().zip(&[subtag!("foo"), subtag!("longstag")]) {
160        assert_eq!(s, reference);
161    }
162
163    let value = Value::try_from_str("foo-ba");
164    assert!(value.is_err());
165}