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::helpers::ShortSlice;
6use crate::parser::{ParserError, SubtagIterator};
7use core::ops::RangeInclusive;
8use core::str::FromStr;
9use tinystr::TinyAsciiStr;
10
11/// A value used in a list of [`Fields`](super::Fields).
12///
13/// The value has to be a sequence of one or more alphanumerical strings
14/// separated by `-`.
15/// Each part of the sequence has to be no shorter than three characters and no
16/// longer than 8.
17///
18/// # Examples
19///
20/// ```
21/// use icu::locid::extensions::transform::Value;
22///
23/// "hybrid".parse::<Value>().expect("Valid Value.");
24///
25/// "hybrid-foobar".parse::<Value>().expect("Valid Value.");
26///
27/// "no".parse::<Value>().expect_err("Invalid Value.");
28/// ```
29#[derive(Debug, PartialEq, Eq, Clone, Hash, PartialOrd, Ord, Default)]
30pub struct Value(ShortSlice<TinyAsciiStr<{ *TYPE_LENGTH.end() }>>);
31
32const TYPE_LENGTH: RangeInclusive<usize> = 3..=8;
33const TRUE_TVALUE: TinyAsciiStr<8> = tinystr::tinystr!(8, "true");
34
35impl Value {
36 /// A constructor which takes a utf8 slice, parses it and
37 /// produces a well-formed [`Value`].
38 ///
39 /// # Examples
40 ///
41 /// ```
42 /// use icu::locid::extensions::transform::Value;
43 ///
44 /// let value = Value::try_from_bytes(b"hybrid").expect("Parsing failed.");
45 /// ```
46 pub fn try_from_bytes(input: &[u8]) -> Result<Self, ParserError> {
47 let mut v = ShortSlice::default();
48 let mut has_value = false;
49
50 for subtag in SubtagIterator::new(input) {
51 if !Self::is_type_subtag(subtag) {
52 return Err(ParserError::InvalidExtension);
53 }
54 has_value = true;
55 let val =
56 TinyAsciiStr::from_bytes(subtag).map_err(|_| ParserError::InvalidExtension)?;
57 if val != TRUE_TVALUE {
58 v.push(val);
59 }
60 }
61
62 if !has_value {
63 return Err(ParserError::InvalidExtension);
64 }
65 Ok(Self(v))
66 }
67
68 pub(crate) fn from_short_slice_unchecked(
69 input: ShortSlice<TinyAsciiStr<{ *TYPE_LENGTH.end() }>>,
70 ) -> Self {
71 Self(input)
72 }
73
74 pub(crate) fn is_type_subtag(t: &[u8]) -> bool {
75 TYPE_LENGTH.contains(&t.len()) && t.iter().all(u8::is_ascii_alphanumeric)
76 }
77
78 pub(crate) fn parse_subtag(
79 t: &[u8],
80 ) -> Result<Option<TinyAsciiStr<{ *TYPE_LENGTH.end() }>>, ParserError> {
81 let s = TinyAsciiStr::from_bytes(t).map_err(|_| ParserError::InvalidSubtag)?;
82 if !TYPE_LENGTH.contains(&t.len()) || !s.is_ascii_alphanumeric() {
83 return Err(ParserError::InvalidExtension);
84 }
85
86 let s = s.to_ascii_lowercase();
87
88 if s == TRUE_TVALUE {
89 Ok(None)
90 } else {
91 Ok(Some(s))
92 }
93 }
94
95 pub(crate) fn for_each_subtag_str<E, F>(&self, f: &mut F) -> Result<(), E>
96 where
97 F: FnMut(&str) -> Result<(), E>,
98 {
99 if self.0.is_empty() {
100 f("true")?;
101 } else {
102 self.0.iter().map(TinyAsciiStr::as_str).try_for_each(f)?;
103 }
104 Ok(())
105 }
106}
107
108impl FromStr for Value {
109 type Err = ParserError;
110
111 fn from_str(source: &str) -> Result<Self, Self::Err> {
112 Self::try_from_bytes(input:source.as_bytes())
113 }
114}
115
116impl_writeable_for_each_subtag_str_no_test!(Value, selff, selff.0.is_empty() => alloc::borrow::Cow::Borrowed("true"));
117
118#[test]
119fn test_writeable() {
120 use writeable::assert_writeable_eq;
121
122 let hybrid = "hybrid".parse().unwrap();
123 let foobar = "foobar".parse().unwrap();
124
125 assert_writeable_eq!(Value::default(), "true");
126 assert_writeable_eq!(
127 Value::from_short_slice_unchecked(vec![hybrid].into()),
128 "hybrid"
129 );
130 assert_writeable_eq!(
131 Value::from_short_slice_unchecked(vec![hybrid, foobar].into()),
132 "hybrid-foobar"
133 );
134}
135