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}