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}