icu_locale_core/
helpers.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
5macro_rules! impl_tinystr_subtag {
6    (
7        $(#[$doc:meta])*
8        $name:ident,
9        $($path:ident)::+,
10        $macro_name:ident,
11        $internal_macro_name:ident,
12        $len_start:literal..=$len_end:literal,
13        $tinystr_ident:ident,
14        $validate:expr,
15        $normalize:expr,
16        $is_normalized:expr,
17        $error:ident,
18        [$good_example:literal $(,$more_good_examples:literal)*],
19        [$bad_example:literal $(, $more_bad_examples:literal)*],
20    ) => {
21        #[derive(Debug, PartialEq, Eq, Clone, Hash, PartialOrd, Ord, Copy)]
22        #[cfg_attr(feature = "serde", derive(serde::Serialize))]
23        #[repr(transparent)]
24        $(#[$doc])*
25        pub struct $name(tinystr::TinyAsciiStr<$len_end>);
26
27        impl $name {
28            /// A constructor which takes a str slice, parses it and
29            #[doc = concat!("produces a well-formed [`", stringify!($name), "`].")]
30            ///
31            /// # Examples
32            ///
33            /// ```
34            #[doc = concat!("use icu_locale_core::", stringify!($($path::)+), stringify!($name), ";")]
35            ///
36            #[doc = concat!("assert!(", stringify!($name), "::try_from_str(", stringify!($good_example), ").is_ok());")]
37            #[doc = concat!("assert!(", stringify!($name), "::try_from_str(", stringify!($bad_example), ").is_err());")]
38            /// ```
39            #[inline]
40            pub const fn try_from_str(s: &str) -> Result<Self, crate::parser::errors::ParseError> {
41                Self::try_from_utf8(s.as_bytes())
42            }
43
44            /// See [`Self::try_from_str`]
45            pub const fn try_from_utf8(
46                code_units: &[u8],
47            ) -> Result<Self, crate::parser::errors::ParseError> {
48                #[allow(clippy::double_comparisons)] // if code_units.len() === 0
49                if code_units.len() < $len_start || code_units.len() > $len_end {
50                    return Err(crate::parser::errors::ParseError::$error);
51                }
52
53                match tinystr::TinyAsciiStr::try_from_utf8(code_units) {
54                    Ok($tinystr_ident) if $validate => Ok(Self($normalize)),
55                    _ => Err(crate::parser::errors::ParseError::$error),
56                }
57            }
58
59            #[doc = concat!("Safely creates a [`", stringify!($name), "`] from its raw format")]
60            /// as returned by [`Self::into_raw`]. Unlike [`Self::try_from_utf8`],
61            /// this constructor only takes normalized values.
62            pub const fn try_from_raw(
63                raw: [u8; $len_end],
64            ) -> Result<Self, crate::parser::errors::ParseError> {
65                if let Ok($tinystr_ident) = tinystr::TinyAsciiStr::<$len_end>::try_from_raw(raw) {
66                    if $tinystr_ident.len() >= $len_start && $is_normalized {
67                        Ok(Self($tinystr_ident))
68                    } else {
69                        Err(crate::parser::errors::ParseError::$error)
70                    }
71                } else {
72                    Err(crate::parser::errors::ParseError::$error)
73                }
74            }
75
76            #[doc = concat!("Unsafely creates a [`", stringify!($name), "`] from its raw format")]
77            /// as returned by [`Self::into_raw`]. Unlike [`Self::try_from_utf8`],
78            /// this constructor only takes normalized values.
79            ///
80            /// # Safety
81            ///
82            /// This function is safe iff [`Self::try_from_raw`] returns an `Ok`. This is the case
83            /// for inputs that are correctly normalized.
84            pub const unsafe fn from_raw_unchecked(v: [u8; $len_end]) -> Self {
85                Self(tinystr::TinyAsciiStr::from_utf8_unchecked(v))
86            }
87
88            /// Deconstructs into a raw format to be consumed by
89            /// [`from_raw_unchecked`](Self::from_raw_unchecked()) or
90            /// [`try_from_raw`](Self::try_from_raw()).
91            pub const fn into_raw(self) -> [u8; $len_end] {
92                *self.0.all_bytes()
93            }
94
95            #[inline]
96            /// A helper function for displaying as a `&str`.
97            pub const fn as_str(&self) -> &str {
98                self.0.as_str()
99            }
100
101            #[doc(hidden)]
102            pub const fn to_tinystr(&self) -> tinystr::TinyAsciiStr<$len_end> {
103                self.0
104            }
105
106            /// Compare with BCP-47 bytes.
107            ///
108            /// The return value is equivalent to what would happen if you first converted
109            /// `self` to a BCP-47 string and then performed a byte comparison.
110            ///
111            /// This function is case-sensitive and results in a *total order*, so it is appropriate for
112            /// binary search. The only argument producing [`Ordering::Equal`](core::cmp::Ordering::Equal)
113            /// is `self.as_str().as_bytes()`.
114            #[inline]
115            pub fn strict_cmp(self, other: &[u8]) -> core::cmp::Ordering {
116                self.as_str().as_bytes().cmp(other)
117            }
118
119            /// Compare with a potentially unnormalized BCP-47 string.
120            ///
121            /// The return value is equivalent to what would happen if you first parsed the
122            /// BCP-47 string and then performed a structural comparison.
123            ///
124            #[inline]
125            pub fn normalizing_eq(self, other: &str) -> bool {
126                self.as_str().eq_ignore_ascii_case(other)
127            }
128        }
129
130        impl core::str::FromStr for $name {
131            type Err = crate::parser::errors::ParseError;
132
133            #[inline]
134            fn from_str(s: &str) -> Result<Self, Self::Err> {
135                Self::try_from_str(s)
136            }
137        }
138
139        impl<'l> From<&'l $name> for &'l str {
140            fn from(input: &'l $name) -> Self {
141                input.as_str()
142            }
143        }
144
145        impl From<$name> for tinystr::TinyAsciiStr<$len_end> {
146            fn from(input: $name) -> Self {
147                input.to_tinystr()
148            }
149        }
150
151        impl writeable::Writeable for $name {
152            #[inline]
153            fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result {
154                sink.write_str(self.as_str())
155            }
156            #[inline]
157            fn writeable_length_hint(&self) -> writeable::LengthHint {
158                writeable::LengthHint::exact(self.0.len())
159            }
160            #[inline]
161            #[cfg(feature = "alloc")]
162            fn write_to_string(&self) -> alloc::borrow::Cow<str> {
163                alloc::borrow::Cow::Borrowed(self.0.as_str())
164            }
165        }
166
167        writeable::impl_display_with_writeable!($name);
168
169        #[doc = concat!("A macro allowing for compile-time construction of valid [`", stringify!($name), "`] subtags.")]
170        ///
171        /// # Examples
172        ///
173        /// Parsing errors don't have to be handled at runtime:
174        /// ```
175        /// assert_eq!(
176        #[doc = concat!("  icu_locale_core::", $(stringify!($path), "::",)+ stringify!($macro_name), "!(", stringify!($good_example) ,"),")]
177        #[doc = concat!("  ", stringify!($good_example), ".parse::<icu_locale_core::", $(stringify!($path), "::",)+ stringify!($name), ">().unwrap()")]
178        /// );
179        /// ```
180        ///
181        /// Invalid input is a compile failure:
182        /// ```compile_fail,E0080
183        #[doc = concat!("icu_locale_core::", $(stringify!($path), "::",)+ stringify!($macro_name), "!(", stringify!($bad_example) ,");")]
184        /// ```
185        ///
186        #[doc = concat!("[`", stringify!($name), "`]: crate::", $(stringify!($path), "::",)+ stringify!($name))]
187        #[macro_export]
188        #[doc(hidden)] // macro
189        macro_rules! $internal_macro_name {
190            ($string:literal) => { const {
191                use $crate::$($path ::)+ $name;
192                match $name::try_from_utf8($string.as_bytes()) {
193                    Ok(r) => r,
194                    #[allow(clippy::panic)] // const context
195                    _ => panic!(concat!("Invalid ", $(stringify!($path), "::",)+ stringify!($name), ": ", $string)),
196                }
197            }};
198        }
199        #[doc(inline)]
200        pub use $internal_macro_name as $macro_name;
201
202        #[cfg(feature = "databake")]
203        impl databake::Bake for $name {
204            fn bake(&self, env: &databake::CrateEnv) -> databake::TokenStream {
205                env.insert("icu_locale_core");
206                let string = self.as_str();
207                databake::quote! { icu_locale_core::$($path::)+ $macro_name!(#string) }
208            }
209        }
210
211        #[cfg(feature = "databake")]
212        impl databake::BakeSize for $name {
213            fn borrows_size(&self) -> usize {
214                0
215            }
216        }
217
218        #[test]
219        fn test_construction() {
220            let maybe = $name::try_from_utf8($good_example.as_bytes());
221            assert!(maybe.is_ok());
222            assert_eq!(maybe, $name::try_from_raw(maybe.unwrap().into_raw()));
223            assert_eq!(maybe.unwrap().as_str(), $good_example);
224            $(
225                let maybe = $name::try_from_utf8($more_good_examples.as_bytes());
226                assert!(maybe.is_ok());
227                assert_eq!(maybe, $name::try_from_raw(maybe.unwrap().into_raw()));
228                assert_eq!(maybe.unwrap().as_str(), $more_good_examples);
229            )*
230            assert!($name::try_from_utf8($bad_example.as_bytes()).is_err());
231            $(
232                assert!($name::try_from_utf8($more_bad_examples.as_bytes()).is_err());
233            )*
234        }
235
236        #[test]
237        fn test_writeable() {
238            writeable::assert_writeable_eq!(&$good_example.parse::<$name>().unwrap(), $good_example);
239            $(
240                writeable::assert_writeable_eq!($more_good_examples.parse::<$name>().unwrap(), $more_good_examples);
241            )*
242        }
243
244        #[cfg(feature = "serde")]
245        impl<'de> serde::Deserialize<'de> for $name {
246            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
247            where
248                D: serde::de::Deserializer<'de>,
249            {
250                struct Visitor;
251
252                impl<'de> serde::de::Visitor<'de> for Visitor {
253                    type Value = $name;
254
255                    fn expecting(
256                        &self,
257                        formatter: &mut core::fmt::Formatter<'_>,
258                    ) -> core::fmt::Result {
259                        write!(formatter, "a valid BCP-47 {}", stringify!($name))
260                    }
261
262                    fn visit_str<E: serde::de::Error>(self, s: &str) -> Result<Self::Value, E> {
263                        s.parse().map_err(serde::de::Error::custom)
264                    }
265                }
266
267                if deserializer.is_human_readable() {
268                    deserializer.deserialize_string(Visitor)
269                } else {
270                    Self::try_from_raw(serde::de::Deserialize::deserialize(deserializer)?)
271                        .map_err(serde::de::Error::custom)
272                }
273            }
274        }
275
276        // Safety checklist for ULE:
277        //
278        // 1. Must not include any uninitialized or padding bytes (true since transparent over a ULE).
279        // 2. Must have an alignment of 1 byte (true since transparent over a ULE).
280        // 3. ULE::validate_bytes() checks that the given byte slice represents a valid slice.
281        // 4. ULE::validate_bytes() checks that the given byte slice has a valid length.
282        // 5. All other methods must be left with their default impl.
283        // 6. Byte equality is semantic equality.
284        #[cfg(feature = "zerovec")]
285        unsafe impl zerovec::ule::ULE for $name {
286            fn validate_bytes(bytes: &[u8]) -> Result<(), zerovec::ule::UleError> {
287                let it = bytes.chunks_exact(core::mem::size_of::<Self>());
288                if !it.remainder().is_empty() {
289                    return Err(zerovec::ule::UleError::length::<Self>(bytes.len()));
290                }
291                for v in it {
292                    // The following can be removed once `array_chunks` is stabilized.
293                    let mut a = [0; core::mem::size_of::<Self>()];
294                    a.copy_from_slice(v);
295                    if Self::try_from_raw(a).is_err() {
296                        return Err(zerovec::ule::UleError::parse::<Self>());
297                    }
298                }
299                Ok(())
300            }
301        }
302
303        #[cfg(feature = "zerovec")]
304        impl zerovec::ule::NicheBytes<$len_end> for $name {
305            const NICHE_BIT_PATTERN: [u8; $len_end] = <tinystr::TinyAsciiStr<$len_end>>::NICHE_BIT_PATTERN;
306        }
307
308        #[cfg(feature = "zerovec")]
309        impl zerovec::ule::AsULE for $name {
310            type ULE = Self;
311            fn to_unaligned(self) -> Self::ULE {
312                self
313            }
314            fn from_unaligned(unaligned: Self::ULE) -> Self {
315                unaligned
316            }
317        }
318
319        #[cfg(feature = "zerovec")]
320        impl<'a> zerovec::maps::ZeroMapKV<'a> for $name {
321            type Container = zerovec::ZeroVec<'a, $name>;
322            type Slice = zerovec::ZeroSlice<$name>;
323            type GetType = $name;
324            type OwnedType = $name;
325        }
326    };
327}
328
329#[macro_export]
330#[doc(hidden)]
331macro_rules! impl_writeable_for_each_subtag_str_no_test {
332    ($type:tt $(, $self:ident, $borrow_cond:expr => $borrow:expr)?) => {
333        impl writeable::Writeable for $type {
334            fn write_to<W: core::fmt::Write + ?Sized>(&self, sink: &mut W) -> core::fmt::Result {
335                let mut initial = true;
336                self.for_each_subtag_str(&mut |subtag| {
337                    if initial {
338                        initial = false;
339                    } else {
340                        sink.write_char('-')?;
341                    }
342                    sink.write_str(subtag)
343                })
344            }
345
346            #[inline]
347            fn writeable_length_hint(&self) -> writeable::LengthHint {
348                let mut result = writeable::LengthHint::exact(0);
349                let mut initial = true;
350                self.for_each_subtag_str::<core::convert::Infallible, _>(&mut |subtag| {
351                    if initial {
352                        initial = false;
353                    } else {
354                        result += 1;
355                    }
356                    result += subtag.len();
357                    Ok(())
358                })
359                .expect("infallible");
360                result
361            }
362
363            $(
364                #[cfg(feature = "alloc")]
365                fn write_to_string(&self) -> alloc::borrow::Cow<str> {
366                    #[allow(clippy::unwrap_used)] // impl_writeable_for_subtag_list's $borrow uses unwrap
367                    let $self = self;
368                    if $borrow_cond {
369                        $borrow
370                    } else {
371                        let mut output = alloc::string::String::with_capacity(self.writeable_length_hint().capacity());
372                        let _ = self.write_to(&mut output);
373                        alloc::borrow::Cow::Owned(output)
374                    }
375                }
376            )?
377        }
378
379        writeable::impl_display_with_writeable!($type);
380    };
381}
382
383macro_rules! impl_writeable_for_subtag_list {
384    ($type:tt, $sample1:literal, $sample2:literal) => {
385        impl_writeable_for_each_subtag_str_no_test!($type, selff, selff.0.len() == 1 => alloc::borrow::Cow::Borrowed(selff.0.get(0).unwrap().as_str()));
386
387        #[test]
388        fn test_writeable() {
389            writeable::assert_writeable_eq!(&$type::default(), "");
390            writeable::assert_writeable_eq!(
391                &$type::from_vec_unchecked(alloc::vec![$sample1.parse().unwrap()]),
392                $sample1,
393            );
394            writeable::assert_writeable_eq!(
395                &$type::from_vec_unchecked(vec![
396                    $sample1.parse().unwrap(),
397                    $sample2.parse().unwrap()
398                ]),
399                core::concat!($sample1, "-", $sample2),
400            );
401        }
402    };
403}
404
405macro_rules! impl_writeable_for_key_value {
406    ($type:tt, $key1:literal, $value1:literal, $key2:literal, $expected2:literal) => {
407        impl_writeable_for_each_subtag_str_no_test!($type);
408
409        #[test]
410        fn test_writeable() {
411            writeable::assert_writeable_eq!(&$type::default(), "");
412            writeable::assert_writeable_eq!(
413                &$type::from_tuple_vec(vec![($key1.parse().unwrap(), $value1.parse().unwrap())]),
414                core::concat!($key1, "-", $value1),
415            );
416            writeable::assert_writeable_eq!(
417                &$type::from_tuple_vec(vec![
418                    ($key1.parse().unwrap(), $value1.parse().unwrap()),
419                    ($key2.parse().unwrap(), "true".parse().unwrap())
420                ]),
421                core::concat!($key1, "-", $value1, "-", $expected2),
422            );
423        }
424    };
425}