| 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 | use crate::parser::{ParserError, SubtagIterator}; |
| 6 | use crate::shortvec::ShortBoxSlice; |
| 7 | use core::ops::RangeInclusive; |
| 8 | use core::str::FromStr; |
| 9 | use 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)] |
| 30 | pub struct Value(ShortBoxSlice<TinyAsciiStr<{ *TYPE_LENGTH.end() }>>); |
| 31 | |
| 32 | const TYPE_LENGTH: RangeInclusive<usize> = 3..=8; |
| 33 | const TRUE_TVALUE: TinyAsciiStr<8> = tinystr::tinystr!(8, "true" ); |
| 34 | |
| 35 | impl 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 = ShortBoxSlice::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: ShortBoxSlice<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 | |
| 108 | impl 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 | |
| 116 | impl_writeable_for_each_subtag_str_no_test!(Value, selff, selff.0.is_empty() => alloc::borrow::Cow::Borrowed("true" )); |
| 117 | |
| 118 | #[test ] |
| 119 | fn 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 | |