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.downcast_ref::<Self>() == Some(self)
64    }
65    fn as_any(&self) -> &dyn Any {
66        self
67    }
68}
69
70/// The `FluentValue` enum represents values which can be formatted to a String.
71///
72/// Those values are either passed as arguments to [`FluentBundle::format_pattern`] or
73/// produced by functions, or generated in the process of pattern resolution.
74///
75/// [`FluentBundle::format_pattern`]: crate::bundle::FluentBundle::format_pattern
76#[derive(Debug)]
77pub enum FluentValue<'source> {
78    String(Cow<'source, str>),
79    Number(FluentNumber),
80    Custom(Box<dyn FluentType + Send>),
81    None,
82    Error,
83}
84
85impl PartialEq for FluentValue<'_> {
86    fn eq(&self, other: &Self) -> bool {
87        match (self, other) {
88            (FluentValue::String(s), FluentValue::String(s2)) => s == s2,
89            (FluentValue::Number(s), FluentValue::Number(s2)) => s == s2,
90            (FluentValue::Custom(s), FluentValue::Custom(s2)) => s == s2,
91            _ => false,
92        }
93    }
94}
95
96impl Clone for FluentValue<'_> {
97    fn clone(&self) -> Self {
98        match self {
99            FluentValue::String(s) => FluentValue::String(s.clone()),
100            FluentValue::Number(s) => FluentValue::Number(s.clone()),
101            FluentValue::Custom(s) => {
102                let new_value: Box<dyn FluentType + Send> = s.duplicate();
103                FluentValue::Custom(new_value)
104            }
105            FluentValue::Error => FluentValue::Error,
106            FluentValue::None => FluentValue::None,
107        }
108    }
109}
110
111impl<'source> FluentValue<'source> {
112    /// Attempts to parse the string representation of a `value` that supports
113    /// [`ToString`] into a [`FluentValue::Number`]. If it fails, it will instead
114    /// convert it to a [`FluentValue::String`].
115    ///
116    /// ```
117    /// use fluent_bundle::types::{FluentNumber, FluentNumberOptions, FluentValue};
118    ///
119    /// // "2" parses into a `FluentNumber`
120    /// assert_eq!(
121    ///     FluentValue::try_number("2"),
122    ///     FluentValue::Number(FluentNumber::new(2.0, FluentNumberOptions::default()))
123    /// );
124    ///
125    /// // Floats can be parsed as well.
126    /// assert_eq!(
127    ///     FluentValue::try_number("3.141569"),
128    ///     FluentValue::Number(FluentNumber::new(
129    ///         3.141569,
130    ///         FluentNumberOptions {
131    ///             minimum_fraction_digits: Some(6),
132    ///             ..Default::default()
133    ///         }
134    ///     ))
135    /// );
136    ///
137    /// // When a value is not a valid number, it falls back to a `FluentValue::String`
138    /// assert_eq!(
139    ///     FluentValue::try_number("A string"),
140    ///     FluentValue::String("A string".into())
141    /// );
142    /// ```
143    pub fn try_number(value: &'source str) -> Self {
144        if let Ok(number) = FluentNumber::from_str(value) {
145            number.into()
146        } else {
147            value.into()
148        }
149    }
150
151    /// Checks to see if two [`FluentValues`](FluentValue) match each other by having the
152    /// same type and contents. The special exception is in the case of a string being
153    /// compared to a number. Here attempt to check that the plural rule category matches.
154    ///
155    /// ```
156    /// use fluent_bundle::resolver::Scope;
157    /// use fluent_bundle::{types::FluentValue, FluentBundle, FluentResource};
158    /// use unic_langid::langid;
159    ///
160    /// let langid_ars = langid!("en");
161    /// let bundle: FluentBundle<FluentResource> = FluentBundle::new(vec![langid_ars]);
162    /// let scope = Scope::new(&bundle, None, None);
163    ///
164    /// // Matching examples:
165    /// assert!(FluentValue::try_number("2").matches(&FluentValue::try_number("2"), &scope));
166    /// assert!(FluentValue::from("fluent").matches(&FluentValue::from("fluent"), &scope));
167    /// assert!(
168    ///     FluentValue::from("one").matches(&FluentValue::try_number("1"), &scope),
169    ///     "Plural rules are matched."
170    /// );
171    ///
172    /// // Non-matching examples:
173    /// assert!(!FluentValue::try_number("2").matches(&FluentValue::try_number("3"), &scope));
174    /// assert!(!FluentValue::from("fluent").matches(&FluentValue::from("not fluent"), &scope));
175    /// assert!(!FluentValue::from("two").matches(&FluentValue::try_number("100"), &scope),);
176    /// ```
177    pub fn matches<R: Borrow<FluentResource>, M>(
178        &self,
179        other: &FluentValue,
180        scope: &Scope<R, M>,
181    ) -> bool
182    where
183        M: MemoizerKind,
184    {
185        match (self, other) {
186            (FluentValue::String(a), FluentValue::String(b)) => a == b,
187            (FluentValue::Number(a), FluentValue::Number(b)) => a == b,
188            (FluentValue::String(a), FluentValue::Number(b)) => {
189                let cat = match a.as_ref() {
190                    "zero" => PluralCategory::ZERO,
191                    "one" => PluralCategory::ONE,
192                    "two" => PluralCategory::TWO,
193                    "few" => PluralCategory::FEW,
194                    "many" => PluralCategory::MANY,
195                    "other" => PluralCategory::OTHER,
196                    _ => return false,
197                };
198                // This string matches a plural rule keyword. Check if the number
199                // matches the plural rule category.
200                let r#type = match b.options.r#type {
201                    FluentNumberType::Cardinal => PluralRuleType::CARDINAL,
202                    FluentNumberType::Ordinal => PluralRuleType::ORDINAL,
203                };
204                scope
205                    .bundle
206                    .intls
207                    .with_try_get_threadsafe::<PluralRules, _, _>((r#type,), |pr| {
208                        pr.0.select(b) == Ok(cat)
209                    })
210                    .unwrap()
211            }
212            _ => false,
213        }
214    }
215
216    /// Write out a string version of the [`FluentValue`] to `W`.
217    pub fn write<W, R, M>(&self, w: &mut W, scope: &Scope<R, M>) -> fmt::Result
218    where
219        W: fmt::Write,
220        R: Borrow<FluentResource>,
221        M: MemoizerKind,
222    {
223        if let Some(formatter) = &scope.bundle.formatter {
224            if let Some(val) = formatter(self, &scope.bundle.intls) {
225                return w.write_str(&val);
226            }
227        }
228        match self {
229            FluentValue::String(s) => w.write_str(s),
230            FluentValue::Number(n) => w.write_str(&n.as_string()),
231            FluentValue::Custom(s) => w.write_str(&scope.bundle.intls.stringify_value(&**s)),
232            FluentValue::Error => Ok(()),
233            FluentValue::None => Ok(()),
234        }
235    }
236
237    /// Converts the [`FluentValue`] to a string.
238    ///
239    /// Clones inner values when owned, borrowed data is not cloned.
240    /// Prefer using [`FluentValue::into_string()`] when possible.
241    pub fn as_string<R: Borrow<FluentResource>, M>(&self, scope: &Scope<R, M>) -> Cow<'source, str>
242    where
243        M: MemoizerKind,
244    {
245        if let Some(formatter) = &scope.bundle.formatter {
246            if let Some(val) = formatter(self, &scope.bundle.intls) {
247                return val.into();
248            }
249        }
250        match self {
251            FluentValue::String(s) => s.clone(),
252            FluentValue::Number(n) => n.as_string(),
253            FluentValue::Custom(s) => scope.bundle.intls.stringify_value(&**s),
254            FluentValue::Error => "".into(),
255            FluentValue::None => "".into(),
256        }
257    }
258
259    /// Converts the [`FluentValue`] to a string.
260    ///
261    /// Takes self by-value to be able to skip expensive clones.
262    /// Prefer this method over [`FluentValue::as_string()`] when possible.
263    pub fn into_string<R: Borrow<FluentResource>, M>(self, scope: &Scope<R, M>) -> Cow<'source, str>
264    where
265        M: MemoizerKind,
266    {
267        if let Some(formatter) = &scope.bundle.formatter {
268            if let Some(val) = formatter(&self, &scope.bundle.intls) {
269                return val.into();
270            }
271        }
272        match self {
273            FluentValue::String(s) => s,
274            FluentValue::Number(n) => n.as_string(),
275            FluentValue::Custom(s) => scope.bundle.intls.stringify_value(s.as_ref()),
276            FluentValue::Error => "".into(),
277            FluentValue::None => "".into(),
278        }
279    }
280
281    pub fn into_owned<'a>(&self) -> FluentValue<'a> {
282        match self {
283            FluentValue::String(str) => FluentValue::String(Cow::from(str.to_string())),
284            FluentValue::Number(s) => FluentValue::Number(s.clone()),
285            FluentValue::Custom(s) => FluentValue::Custom(s.duplicate()),
286            FluentValue::Error => FluentValue::Error,
287            FluentValue::None => FluentValue::None,
288        }
289    }
290}
291
292impl From<String> for FluentValue<'_> {
293    fn from(s: String) -> Self {
294        FluentValue::String(s.into())
295    }
296}
297
298impl<'source> From<&'source String> for FluentValue<'source> {
299    fn from(s: &'source String) -> Self {
300        FluentValue::String(s.into())
301    }
302}
303
304impl<'source> From<&'source str> for FluentValue<'source> {
305    fn from(s: &'source str) -> Self {
306        FluentValue::String(s.into())
307    }
308}
309
310impl<'source> From<Cow<'source, str>> for FluentValue<'source> {
311    fn from(s: Cow<'source, str>) -> Self {
312        FluentValue::String(s)
313    }
314}
315
316impl<'source, T> From<Option<T>> for FluentValue<'source>
317where
318    T: Into<FluentValue<'source>>,
319{
320    fn from(v: Option<T>) -> Self {
321        match v {
322            Some(v) => v.into(),
323            None => FluentValue::None,
324        }
325    }
326}