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