fluent_bundle/types/
mod.rs

1//! `types` module contains types necessary for Fluent runtime
2//! value handling.
3//! The core struct is [`FluentValue`] which is a type that can be passed
4//! to the [`FluentBundle::format_pattern`](crate::bundle::FluentBundle) as an argument, it can be passed
5//! to any Fluent Function, and any function may return it.
6//!
7//! This part of functionality is not fully hashed out yet, since we're waiting
8//! for the internationalization APIs to mature, at which point all number
9//! formatting operations will be moved out of Fluent.
10//!
11//! For now, [`FluentValue`] can be a string, a number, or a custom [`FluentType`]
12//! which allows users of the library to implement their own types of values,
13//! such as dates, or more complex structures needed for their bindings.
14mod number;
15mod plural;
16
17pub use number::*;
18use plural::PluralRules;
19
20use std::any::Any;
21use std::borrow::{Borrow, Cow};
22use std::fmt;
23use std::str::FromStr;
24
25use intl_pluralrules::{PluralCategory, PluralRuleType};
26
27use crate::memoizer::MemoizerKind;
28use crate::resolver::Scope;
29use crate::resource::FluentResource;
30
31/// Custom types can implement the [`FluentType`] trait in order to generate a string
32/// value for use in the message generation process.
33pub trait FluentType: fmt::Debug + AnyEq + 'static {
34    /// Create a clone of the underlying type.
35    fn duplicate(&self) -> Box<dyn FluentType + Send>;
36
37    /// Convert the custom type into a string value, for instance a custom DateTime
38    /// type could return "Oct. 27, 2022".
39    fn as_string(&self, intls: &intl_memoizer::IntlLangMemoizer) -> Cow<'static, str>;
40
41    /// Convert the custom type into a string value, for instance a custom DateTime
42    /// type could return "Oct. 27, 2022". This operation is provided the threadsafe
43    /// [IntlLangMemoizer](intl_memoizer::concurrent::IntlLangMemoizer).
44    fn as_string_threadsafe(
45        &self,
46        intls: &intl_memoizer::concurrent::IntlLangMemoizer,
47    ) -> Cow<'static, str>;
48}
49
50impl PartialEq for dyn FluentType + Send {
51    fn eq(&self, other: &Self) -> bool {
52        self.equals(other.as_any())
53    }
54}
55
56pub trait AnyEq: Any + 'static {
57    fn equals(&self, other: &dyn Any) -> bool;
58    fn as_any(&self) -> &dyn Any;
59}
60
61impl<T: Any + PartialEq> AnyEq for T {
62    fn equals(&self, other: &dyn Any) -> bool {
63        other
64            .downcast_ref::<Self>()
65            .map_or(false, |that| self == that)
66    }
67    fn as_any(&self) -> &dyn Any {
68        self
69    }
70}
71
72/// The `FluentValue` enum represents values which can be formatted to a String.
73///
74/// Those values are either passed as arguments to [`FluentBundle::format_pattern`] or
75/// produced by functions, or generated in the process of pattern resolution.
76///
77/// [`FluentBundle::format_pattern`]: crate::bundle::FluentBundle::format_pattern
78#[derive(Debug)]
79pub enum FluentValue<'source> {
80    String(Cow<'source, str>),
81    Number(FluentNumber),
82    Custom(Box<dyn FluentType + Send>),
83    None,
84    Error,
85}
86
87impl<'s> PartialEq for FluentValue<'s> {
88    fn eq(&self, other: &Self) -> bool {
89        match (self, other) {
90            (FluentValue::String(s), FluentValue::String(s2)) => s == s2,
91            (FluentValue::Number(s), FluentValue::Number(s2)) => s == s2,
92            (FluentValue::Custom(s), FluentValue::Custom(s2)) => s == s2,
93            _ => false,
94        }
95    }
96}
97
98impl<'s> Clone for FluentValue<'s> {
99    fn clone(&self) -> Self {
100        match self {
101            FluentValue::String(s) => FluentValue::String(s.clone()),
102            FluentValue::Number(s) => FluentValue::Number(s.clone()),
103            FluentValue::Custom(s) => {
104                let new_value: Box<dyn FluentType + Send> = s.duplicate();
105                FluentValue::Custom(new_value)
106            }
107            FluentValue::Error => FluentValue::Error,
108            FluentValue::None => FluentValue::None,
109        }
110    }
111}
112
113impl<'source> FluentValue<'source> {
114    /// Attempts to parse the string representation of a `value` that supports
115    /// [`ToString`] into a [`FluentValue::Number`]. If it fails, it will instead
116    /// convert it to a [`FluentValue::String`].
117    ///
118    /// ```
119    /// use fluent_bundle::types::{FluentNumber, FluentNumberOptions, FluentValue};
120    ///
121    /// // "2" parses into a `FluentNumber`
122    /// assert_eq!(
123    ///     FluentValue::try_number("2"),
124    ///     FluentValue::Number(FluentNumber::new(2.0, FluentNumberOptions::default()))
125    /// );
126    ///
127    /// // Floats can be parsed as well.
128    /// assert_eq!(
129    ///     FluentValue::try_number("3.141569"),
130    ///     FluentValue::Number(FluentNumber::new(
131    ///         3.141569,
132    ///         FluentNumberOptions {
133    ///             minimum_fraction_digits: Some(6),
134    ///             ..Default::default()
135    ///         }
136    ///     ))
137    /// );
138    ///
139    /// // When a value is not a valid number, it falls back to a `FluentValue::String`
140    /// assert_eq!(
141    ///     FluentValue::try_number("A string"),
142    ///     FluentValue::String("A string".into())
143    /// );
144    /// ```
145    pub fn try_number(value: &'source str) -> Self {
146        if let Ok(number) = FluentNumber::from_str(value) {
147            number.into()
148        } else {
149            value.into()
150        }
151    }
152
153    /// Checks to see if two [`FluentValues`](FluentValue) match each other by having the
154    /// same type and contents. The special exception is in the case of a string being
155    /// compared to a number. Here attempt to check that the plural rule category matches.
156    ///
157    /// ```
158    /// use fluent_bundle::resolver::Scope;
159    /// use fluent_bundle::{types::FluentValue, FluentBundle, FluentResource};
160    /// use unic_langid::langid;
161    ///
162    /// let langid_ars = langid!("en");
163    /// let bundle: FluentBundle<FluentResource> = FluentBundle::new(vec![langid_ars]);
164    /// let scope = Scope::new(&bundle, None, None);
165    ///
166    /// // Matching examples:
167    /// assert!(FluentValue::try_number("2").matches(&FluentValue::try_number("2"), &scope));
168    /// assert!(FluentValue::from("fluent").matches(&FluentValue::from("fluent"), &scope));
169    /// assert!(
170    ///     FluentValue::from("one").matches(&FluentValue::try_number("1"), &scope),
171    ///     "Plural rules are matched."
172    /// );
173    ///
174    /// // Non-matching examples:
175    /// assert!(!FluentValue::try_number("2").matches(&FluentValue::try_number("3"), &scope));
176    /// assert!(!FluentValue::from("fluent").matches(&FluentValue::from("not fluent"), &scope));
177    /// assert!(!FluentValue::from("two").matches(&FluentValue::try_number("100"), &scope),);
178    /// ```
179    pub fn matches<R: Borrow<FluentResource>, M>(
180        &self,
181        other: &FluentValue,
182        scope: &Scope<R, M>,
183    ) -> bool
184    where
185        M: MemoizerKind,
186    {
187        match (self, other) {
188            (&FluentValue::String(ref a), &FluentValue::String(ref b)) => a == b,
189            (&FluentValue::Number(ref a), &FluentValue::Number(ref b)) => a == b,
190            (&FluentValue::String(ref a), &FluentValue::Number(ref b)) => {
191                let cat = match a.as_ref() {
192                    "zero" => PluralCategory::ZERO,
193                    "one" => PluralCategory::ONE,
194                    "two" => PluralCategory::TWO,
195                    "few" => PluralCategory::FEW,
196                    "many" => PluralCategory::MANY,
197                    "other" => PluralCategory::OTHER,
198                    _ => return false,
199                };
200                // This string matches a plural rule keyword. Check if the number
201                // matches the plural rule category.
202                scope
203                    .bundle
204                    .intls
205                    .with_try_get_threadsafe::<PluralRules, _, _>(
206                        (PluralRuleType::CARDINAL,),
207                        |pr| pr.0.select(b) == Ok(cat),
208                    )
209                    .unwrap()
210            }
211            _ => false,
212        }
213    }
214
215    /// Write out a string version of the [`FluentValue`] to `W`.
216    pub fn write<W, R, M>(&self, w: &mut W, scope: &Scope<R, M>) -> fmt::Result
217    where
218        W: fmt::Write,
219        R: Borrow<FluentResource>,
220        M: MemoizerKind,
221    {
222        if let Some(formatter) = &scope.bundle.formatter {
223            if let Some(val) = formatter(self, &scope.bundle.intls) {
224                return w.write_str(&val);
225            }
226        }
227        match self {
228            FluentValue::String(s) => w.write_str(s),
229            FluentValue::Number(n) => w.write_str(&n.as_string()),
230            FluentValue::Custom(s) => w.write_str(&scope.bundle.intls.stringify_value(&**s)),
231            FluentValue::Error => Ok(()),
232            FluentValue::None => Ok(()),
233        }
234    }
235
236    /// Converts the [`FluentValue`] to a string.
237    ///
238    /// Clones inner values when owned, borrowed data is not cloned.
239    /// Prefer using [`FluentValue::into_string()`] when possible.
240    pub fn as_string<R: Borrow<FluentResource>, M>(&self, scope: &Scope<R, M>) -> Cow<'source, str>
241    where
242        M: MemoizerKind,
243    {
244        if let Some(formatter) = &scope.bundle.formatter {
245            if let Some(val) = formatter(self, &scope.bundle.intls) {
246                return val.into();
247            }
248        }
249        match self {
250            FluentValue::String(s) => s.clone(),
251            FluentValue::Number(n) => n.as_string(),
252            FluentValue::Custom(s) => scope.bundle.intls.stringify_value(&**s),
253            FluentValue::Error => "".into(),
254            FluentValue::None => "".into(),
255        }
256    }
257
258    /// Converts the [`FluentValue`] to a string.
259    ///
260    /// Takes self by-value to be able to skip expensive clones.
261    /// Prefer this method over [`FluentValue::as_string()`] when possible.
262    pub fn into_string<R: Borrow<FluentResource>, M>(self, scope: &Scope<R, M>) -> Cow<'source, str>
263    where
264        M: MemoizerKind,
265    {
266        if let Some(formatter) = &scope.bundle.formatter {
267            if let Some(val) = formatter(&self, &scope.bundle.intls) {
268                return val.into();
269            }
270        }
271        match self {
272            FluentValue::String(s) => s,
273            FluentValue::Number(n) => n.as_string(),
274            FluentValue::Custom(s) => scope.bundle.intls.stringify_value(s.as_ref()),
275            FluentValue::Error => "".into(),
276            FluentValue::None => "".into(),
277        }
278    }
279
280    pub fn into_owned<'a>(&self) -> FluentValue<'a> {
281        match self {
282            FluentValue::String(str) => FluentValue::String(Cow::from(str.to_string())),
283            FluentValue::Number(s) => FluentValue::Number(s.clone()),
284            FluentValue::Custom(s) => FluentValue::Custom(s.duplicate()),
285            FluentValue::Error => FluentValue::Error,
286            FluentValue::None => FluentValue::None,
287        }
288    }
289}
290
291impl<'source> From<String> for FluentValue<'source> {
292    fn from(s: String) -> Self {
293        FluentValue::String(s.into())
294    }
295}
296
297impl<'source> From<&'source String> for FluentValue<'source> {
298    fn from(s: &'source String) -> Self {
299        FluentValue::String(s.into())
300    }
301}
302
303impl<'source> From<&'source str> for FluentValue<'source> {
304    fn from(s: &'source str) -> Self {
305        FluentValue::String(s.into())
306    }
307}
308
309impl<'source> From<Cow<'source, str>> for FluentValue<'source> {
310    fn from(s: Cow<'source, str>) -> Self {
311        FluentValue::String(s)
312    }
313}
314
315impl<'source, T> From<Option<T>> for FluentValue<'source>
316where
317    T: Into<FluentValue<'source>>,
318{
319    fn from(v: Option<T>) -> Self {
320        match v {
321            Some(v) => v.into(),
322            None => FluentValue::None,
323        }
324    }
325}