icu_pattern/frontend/
mod.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
5#[cfg(feature = "databake")]
6mod databake;
7#[cfg(feature = "serde")]
8pub(crate) mod serde;
9#[cfg(feature = "zerovec")]
10mod zerovec;
11
12use crate::common::*;
13#[cfg(feature = "alloc")]
14use crate::Error;
15#[cfg(feature = "alloc")]
16use crate::Parser;
17#[cfg(feature = "alloc")]
18use crate::ParserOptions;
19#[cfg(feature = "alloc")]
20use alloc::{borrow::ToOwned, boxed::Box, str::FromStr, string::String};
21use core::{convert::Infallible, fmt, marker::PhantomData};
22use writeable::{adapters::TryWriteableInfallibleAsWriteable, PartsWrite, TryWriteable, Writeable};
23
24/// A string pattern with placeholders.
25///
26/// There are 2 generic parameters: `Backend` and `Store`.
27///
28/// # Backend
29///
30/// This determines the nature of placeholders and serialized encoding of the pattern.
31///
32/// The following backends are available:
33///
34/// - [`SinglePlaceholder`] for patterns with up to one placeholder: `"{0} days ago"`
35/// - [`DoublePlaceholder`] for patterns with up to two placeholders: `"{0} days, {1} hours ago"`
36/// - [`MultiNamedPlaceholder`] for patterns with named placeholders: `"{name} sent you a message"`
37///
38/// # Format to Parts
39///
40/// [`Pattern`] propagates [`Part`]s from inner writeables. In addition, it supports annotating
41/// [`Part`]s for individual literals or placeholders via the [`PlaceholderValueProvider`] trait.
42///
43/// # Examples
44///
45/// Interpolating a [`SinglePlaceholder`] pattern:
46///
47/// ```
48/// use core::str::FromStr;
49/// use icu_pattern::Pattern;
50/// use icu_pattern::SinglePlaceholder;
51/// use writeable::assert_writeable_eq;
52///
53/// let pattern = Pattern::<SinglePlaceholder>::try_from_str(
54///     "Hello, {0}!",
55///     Default::default(),
56/// )
57/// .unwrap();
58///
59/// assert_writeable_eq!(pattern.interpolate(["Alice"]), "Hello, Alice!");
60/// ```
61///
62/// [`SinglePlaceholder`]: crate::SinglePlaceholder
63/// [`DoublePlaceholder`]: crate::DoublePlaceholder
64/// [`MultiNamedPlaceholder`]: crate::MultiNamedPlaceholder
65/// [`Part`]: writeable::Part
66#[cfg_attr(feature = "yoke", derive(yoke::Yokeable))]
67#[repr(transparent)]
68pub struct Pattern<B: PatternBackend> {
69    _backend: PhantomData<B>,
70    /// The encoded storage
71    pub store: B::Store,
72}
73
74impl<B: PatternBackend> PartialEq for Pattern<B> {
75    fn eq(&self, other: &Self) -> bool {
76        self.store == other.store
77    }
78}
79
80impl<B: PatternBackend> core::fmt::Debug for Pattern<B> {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        f.debug_struct("Pattern")
83            .field("_backend", &self._backend)
84            .field("store", &&self.store)
85            .finish()
86    }
87}
88
89impl<B: PatternBackend> Default for &'static Pattern<B> {
90    fn default() -> Self {
91        Pattern::from_ref_store_unchecked(B::empty())
92    }
93}
94
95#[cfg(feature = "alloc")]
96impl<B: PatternBackend> Default for Box<Pattern<B>>
97where
98    Box<B::Store>: From<&'static B::Store>,
99{
100    fn default() -> Self {
101        Pattern::from_boxed_store_unchecked(Box::from(B::empty()))
102    }
103}
104
105#[test]
106fn test_defaults() {
107    assert_eq!(
108        Box::<Pattern::<crate::SinglePlaceholder>>::default(),
109        Pattern::try_from_items(core::iter::empty()).unwrap()
110    );
111    assert_eq!(
112        Box::<Pattern::<crate::DoublePlaceholder>>::default(),
113        Pattern::try_from_items(core::iter::empty()).unwrap()
114    );
115    assert_eq!(
116        Box::<Pattern::<crate::MultiNamedPlaceholder>>::default(),
117        Pattern::try_from_items(core::iter::empty()).unwrap()
118    );
119}
120
121#[cfg(feature = "alloc")]
122impl<B: PatternBackend> ToOwned for Pattern<B>
123where
124    Box<B::Store>: for<'a> From<&'a B::Store>,
125{
126    type Owned = Box<Pattern<B>>;
127
128    fn to_owned(&self) -> Self::Owned {
129        Self::from_boxed_store_unchecked(Box::from(&self.store))
130    }
131}
132
133#[cfg(feature = "alloc")]
134impl<B: PatternBackend> Clone for Box<Pattern<B>>
135where
136    Box<B::Store>: for<'a> From<&'a B::Store>,
137{
138    fn clone(&self) -> Self {
139        Pattern::from_boxed_store_unchecked(Box::from(&self.store))
140    }
141}
142
143impl<B: PatternBackend> Pattern<B> {
144    #[cfg(feature = "alloc")]
145    pub(crate) const fn from_boxed_store_unchecked(store: Box<B::Store>) -> Box<Self> {
146        // Safety: Pattern is repr(transparent) over B::Store
147        unsafe { core::mem::transmute(store) }
148    }
149
150    #[doc(hidden)] // databake
151    pub const fn from_ref_store_unchecked(store: &B::Store) -> &Self {
152        // Safety: Pattern is repr(transparent) over B::Store
153        unsafe { &*(store as *const B::Store as *const Self) }
154    }
155}
156
157#[cfg(feature = "alloc")]
158impl<B> Pattern<B>
159where
160    B: PatternBackend,
161{
162    /// Creates a pattern from an iterator of pattern items.
163    ///
164    /// ✨ *Enabled with the `alloc` Cargo feature.*
165    ///
166    /// # Examples
167    ///
168    /// ```
169    /// use icu_pattern::Pattern;
170    /// use icu_pattern::PatternItemCow;
171    /// use icu_pattern::SinglePlaceholder;
172    /// use icu_pattern::SinglePlaceholderKey;
173    /// use std::borrow::Cow;
174    ///
175    /// Pattern::<SinglePlaceholder>::try_from_items(
176    ///     [
177    ///         PatternItemCow::Placeholder(SinglePlaceholderKey::Singleton),
178    ///         PatternItemCow::Literal(Cow::Borrowed(" days")),
179    ///     ]
180    ///     .into_iter(),
181    /// )
182    /// .expect("valid pattern items");
183    /// ```
184    pub fn try_from_items<'a, I>(items: I) -> Result<Box<Self>, Error>
185    where
186        I: Iterator<Item = PatternItemCow<'a, B::PlaceholderKeyCow<'a>>>,
187    {
188        let store = B::try_from_items(items.map(Ok))?;
189        #[cfg(debug_assertions)]
190        match B::validate_store(core::borrow::Borrow::borrow(&store)) {
191            Ok(()) => (),
192            Err(e) => {
193                debug_assert!(false, "{e:?}");
194            }
195        };
196        Ok(Self::from_boxed_store_unchecked(store))
197    }
198}
199
200#[cfg(feature = "alloc")]
201impl<'a, B> Pattern<B>
202where
203    B: PatternBackend,
204    B::PlaceholderKeyCow<'a>: FromStr,
205    <B::PlaceholderKeyCow<'a> as FromStr>::Err: fmt::Debug,
206{
207    /// Creates a pattern by parsing a syntax string.
208    ///
209    /// ✨ *Enabled with the `alloc` Cargo feature.*
210    ///
211    /// # Examples
212    ///
213    /// ```
214    /// use icu_pattern::Pattern;
215    /// use icu_pattern::SinglePlaceholder;
216    ///
217    /// // Create a pattern from a valid string:
218    /// Pattern::<SinglePlaceholder>::try_from_str("{0} days", Default::default())
219    ///     .expect("valid pattern");
220    ///
221    /// // Error on an invalid pattern:
222    /// Pattern::<SinglePlaceholder>::try_from_str("{0 days", Default::default())
223    ///     .expect_err("mismatched braces");
224    /// ```
225    pub fn try_from_str(pattern: &str, options: ParserOptions) -> Result<Box<Self>, Error> {
226        let parser = Parser::new(pattern, options);
227        let store = B::try_from_items(parser)?;
228        #[cfg(debug_assertions)]
229        match B::validate_store(core::borrow::Borrow::borrow(&store)) {
230            Ok(()) => (),
231            Err(e) => {
232                debug_assert!(false, "{e:?} for pattern {pattern:?}");
233            }
234        };
235        Ok(Self::from_boxed_store_unchecked(store))
236    }
237}
238
239impl<B> Pattern<B>
240where
241    B: PatternBackend,
242{
243    /// Returns an iterator over the [`PatternItem`]s in this pattern.
244    pub fn iter(&self) -> impl Iterator<Item = PatternItem<'_, B::PlaceholderKey<'_>>> + '_ {
245        B::iter_items(&self.store)
246    }
247
248    /// Returns a [`TryWriteable`] that interpolates items from the given replacement provider
249    /// into this pattern string.
250    pub fn try_interpolate<'a, P>(
251        &'a self,
252        value_provider: P,
253    ) -> impl TryWriteable<Error = B::Error<'a>> + fmt::Display + 'a
254    where
255        P: PlaceholderValueProvider<B::PlaceholderKey<'a>, Error = B::Error<'a>> + 'a,
256    {
257        WriteablePattern::<B, P> {
258            store: &self.store,
259            value_provider,
260        }
261    }
262
263    #[cfg(feature = "alloc")]
264    /// Interpolates the pattern directly to a string, returning the string or an error.
265    ///
266    /// In addition to the error, the lossy fallback string is returned in the failure case.
267    ///
268    /// ✨ *Enabled with the `alloc` Cargo feature.*
269    pub fn try_interpolate_to_string<'a, P>(
270        &'a self,
271        value_provider: P,
272    ) -> Result<String, (B::Error<'a>, String)>
273    where
274        P: PlaceholderValueProvider<B::PlaceholderKey<'a>, Error = B::Error<'a>> + 'a,
275    {
276        self.try_interpolate(value_provider)
277            .try_write_to_string()
278            .map(|s| s.into_owned())
279            .map_err(|(e, s)| (e, s.into_owned()))
280    }
281}
282
283impl<B> Pattern<B>
284where
285    for<'b> B: PatternBackend<Error<'b> = Infallible>,
286{
287    /// Returns a [`Writeable`] that interpolates items from the given replacement provider
288    /// into this pattern string.
289    pub fn interpolate<'a, P>(&'a self, value_provider: P) -> impl Writeable + fmt::Display + 'a
290    where
291        P: PlaceholderValueProvider<B::PlaceholderKey<'a>, Error = B::Error<'a>> + 'a,
292    {
293        TryWriteableInfallibleAsWriteable(WriteablePattern::<B, P> {
294            store: &self.store,
295            value_provider,
296        })
297    }
298
299    #[cfg(feature = "alloc")]
300    /// Interpolates the pattern directly to a string.
301    ///
302    /// ✨ *Enabled with the `alloc` Cargo feature.*
303    pub fn interpolate_to_string<'a, P>(&'a self, value_provider: P) -> String
304    where
305        P: PlaceholderValueProvider<B::PlaceholderKey<'a>, Error = B::Error<'a>> + 'a,
306    {
307        self.interpolate(value_provider)
308            .write_to_string()
309            .into_owned()
310    }
311}
312
313struct WriteablePattern<'a, B: PatternBackend, P> {
314    store: &'a B::Store,
315    value_provider: P,
316}
317
318impl<'a, B, P> TryWriteable for WriteablePattern<'a, B, P>
319where
320    B: PatternBackend,
321    P: PlaceholderValueProvider<B::PlaceholderKey<'a>, Error = B::Error<'a>>,
322{
323    type Error = B::Error<'a>;
324
325    fn try_write_to_parts<S: PartsWrite + ?Sized>(
326        &self,
327        sink: &mut S,
328    ) -> Result<Result<(), Self::Error>, fmt::Error> {
329        let mut error = None;
330        let it = B::iter_items(self.store);
331        #[cfg(debug_assertions)]
332        let (size_hint, mut actual_len) = (it.size_hint(), 0);
333        for item in it {
334            match item {
335                PatternItem::Literal(s) => {
336                    self.value_provider.map_literal(s).write_to_parts(sink)?;
337                }
338                PatternItem::Placeholder(key) => {
339                    let element_writeable = self.value_provider.value_for(key);
340                    if let Err(e) = element_writeable.try_write_to_parts(sink)? {
341                        // Keep the first error if there was one
342                        error.get_or_insert(e);
343                    }
344                }
345            }
346            #[cfg(debug_assertions)]
347            {
348                actual_len += 1;
349            }
350        }
351        #[cfg(debug_assertions)]
352        {
353            debug_assert!(actual_len >= size_hint.0);
354            if let Some(max_len) = size_hint.1 {
355                debug_assert!(actual_len <= max_len);
356            }
357        }
358        if let Some(e) = error {
359            Ok(Err(e))
360        } else {
361            Ok(Ok(()))
362        }
363    }
364}
365
366impl<'a, B, P> fmt::Display for WriteablePattern<'a, B, P>
367where
368    B: PatternBackend,
369    P: PlaceholderValueProvider<B::PlaceholderKey<'a>, Error = B::Error<'a>>,
370{
371    #[inline]
372    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
373        // Discard the TryWriteable error (lossy mode)
374        self.try_write_to(f).map(|_| ())
375    }
376}
377
378#[test]
379fn test_try_from_str_inference() {
380    use crate::SinglePlaceholder;
381    let _: Box<Pattern<SinglePlaceholder>> =
382        Pattern::try_from_str("{0} days", Default::default()).unwrap();
383    let _ = Pattern::<SinglePlaceholder>::try_from_str("{0} days", Default::default()).unwrap();
384}