• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

projectfluent / fluent-rs / 13221860010

09 Feb 2025 03:13AM UTC coverage: 89.64% (-0.04%) from 89.675%
13221860010

Pull #378

github

web-flow
Merge ee3bfef45 into f2033ce83
Pull Request #378: deps: update `thiserror` to version 2

3963 of 4421 relevant lines covered (89.64%)

3602.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.14
/fluent-bundle/src/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.
14
mod number;
15
mod plural;
16

17
pub use number::*;
18
use plural::PluralRules;
19

20
use std::any::Any;
21
use std::borrow::{Borrow, Cow};
22
use std::fmt;
23
use std::str::FromStr;
24

25
use intl_pluralrules::{PluralCategory, PluralRuleType};
26

27
use crate::memoizer::MemoizerKind;
28
use crate::resolver::Scope;
29
use 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.
33
pub 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

50
impl PartialEq for dyn FluentType + Send {
51
    fn eq(&self, other: &Self) -> bool {
26✔
52
        self.equals(other.as_any())
26✔
53
    }
26✔
54
}
55

56
pub trait AnyEq: Any + 'static {
57
    fn equals(&self, other: &dyn Any) -> bool;
58
    fn as_any(&self) -> &dyn Any;
59
}
60

61
impl<T: Any + PartialEq> AnyEq for T {
62
    fn equals(&self, other: &dyn Any) -> bool {
2✔
63
        other
2✔
64
            .downcast_ref::<Self>()
2✔
65
            .map_or(false, |that| self == that)
2✔
66
    }
2✔
67
    fn as_any(&self) -> &dyn Any {
3✔
68
        self
3✔
69
    }
3✔
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)]
79
pub enum FluentValue<'source> {
80
    String(Cow<'source, str>),
81
    Number(FluentNumber),
82
    Custom(Box<dyn FluentType + Send>),
83
    None,
84
    Error,
85
}
86

87
impl<'s> PartialEq for FluentValue<'s> {
88
    fn eq(&self, other: &Self) -> bool {
148✔
89
        match (self, other) {
148✔
90
            (FluentValue::String(s), FluentValue::String(s2)) => s == s2,
67✔
91
            (FluentValue::Number(s), FluentValue::Number(s2)) => s == s2,
42✔
92
            (FluentValue::Custom(s), FluentValue::Custom(s2)) => s == s2,
26✔
93
            _ => false,
13✔
94
        }
95
    }
148✔
96
}
97

98
impl<'s> Clone for FluentValue<'s> {
99
    fn clone(&self) -> Self {
182✔
100
        match self {
182✔
101
            FluentValue::String(s) => FluentValue::String(s.clone()),
130✔
102
            FluentValue::Number(s) => FluentValue::Number(s.clone()),
52✔
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
    }
182✔
111
}
112

113
impl<'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 {
770✔
146
        if let Ok(number) = FluentNumber::from_str(value) {
770✔
147
            number.into()
757✔
148
        } else {
149
            value.into()
13✔
150
        }
151
    }
770✔
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>(
114✔
180
        &self,
114✔
181
        other: &FluentValue,
114✔
182
        scope: &Scope<R, M>,
114✔
183
    ) -> bool
114✔
184
    where
114✔
185
        M: MemoizerKind,
114✔
186
    {
114✔
187
        match (self, other) {
114✔
188
            (FluentValue::String(a), FluentValue::String(b)) => a == b,
58✔
189
            (FluentValue::Number(a), FluentValue::Number(b)) => a == b,
14✔
190
            (FluentValue::String(a), FluentValue::Number(b)) => {
41✔
191
                let cat = match a.as_ref() {
41✔
192
                    "zero" => PluralCategory::ZERO,
41✔
193
                    "one" => PluralCategory::ONE,
40✔
194
                    "two" => PluralCategory::TWO,
29✔
195
                    "few" => PluralCategory::FEW,
26✔
196
                    "many" => PluralCategory::MANY,
24✔
197
                    "other" => PluralCategory::OTHER,
23✔
198
                    _ => return false,
9✔
199
                };
200
                // This string matches a plural rule keyword. Check if the number
201
                // matches the plural rule category.
202
                let r#type = match b.options.r#type {
32✔
203
                    FluentNumberType::Cardinal => PluralRuleType::CARDINAL,
20✔
204
                    FluentNumberType::Ordinal => PluralRuleType::ORDINAL,
12✔
205
                };
206
                scope
32✔
207
                    .bundle
32✔
208
                    .intls
32✔
209
                    .with_try_get_threadsafe::<PluralRules, _, _>((r#type,), |pr| {
32✔
210
                        pr.0.select(b) == Ok(cat)
32✔
211
                    })
32✔
212
                    .unwrap()
32✔
213
            }
214
            _ => false,
1✔
215
        }
216
    }
114✔
217

218
    /// Write out a string version of the [`FluentValue`] to `W`.
219
    pub fn write<W, R, M>(&self, w: &mut W, scope: &Scope<R, M>) -> fmt::Result
44✔
220
    where
44✔
221
        W: fmt::Write,
44✔
222
        R: Borrow<FluentResource>,
44✔
223
        M: MemoizerKind,
44✔
224
    {
44✔
225
        if let Some(formatter) = &scope.bundle.formatter {
44✔
226
            if let Some(val) = formatter(self, &scope.bundle.intls) {
1✔
227
                return w.write_str(&val);
1✔
228
            }
×
229
        }
43✔
230
        match self {
43✔
231
            FluentValue::String(s) => w.write_str(s),
11✔
232
            FluentValue::Number(n) => w.write_str(&n.as_string()),
32✔
233
            FluentValue::Custom(s) => w.write_str(&scope.bundle.intls.stringify_value(&**s)),
×
234
            FluentValue::Error => Ok(()),
×
235
            FluentValue::None => Ok(()),
×
236
        }
237
    }
44✔
238

239
    /// Converts the [`FluentValue`] to a string.
240
    ///
241
    /// Clones inner values when owned, borrowed data is not cloned.
242
    /// Prefer using [`FluentValue::into_string()`] when possible.
243
    pub fn as_string<R: Borrow<FluentResource>, M>(&self, scope: &Scope<R, M>) -> Cow<'source, str>
×
244
    where
×
245
        M: MemoizerKind,
×
246
    {
×
247
        if let Some(formatter) = &scope.bundle.formatter {
×
248
            if let Some(val) = formatter(self, &scope.bundle.intls) {
×
249
                return val.into();
×
250
            }
×
251
        }
×
252
        match self {
×
253
            FluentValue::String(s) => s.clone(),
×
254
            FluentValue::Number(n) => n.as_string(),
×
255
            FluentValue::Custom(s) => scope.bundle.intls.stringify_value(&**s),
×
256
            FluentValue::Error => "".into(),
×
257
            FluentValue::None => "".into(),
×
258
        }
259
    }
×
260

261
    /// Converts the [`FluentValue`] to a string.
262
    ///
263
    /// Takes self by-value to be able to skip expensive clones.
264
    /// Prefer this method over [`FluentValue::as_string()`] when possible.
265
    pub fn into_string<R: Borrow<FluentResource>, M>(self, scope: &Scope<R, M>) -> Cow<'source, str>
214✔
266
    where
214✔
267
        M: MemoizerKind,
214✔
268
    {
214✔
269
        if let Some(formatter) = &scope.bundle.formatter {
214✔
270
            if let Some(val) = formatter(&self, &scope.bundle.intls) {
1✔
271
                return val.into();
×
272
            }
1✔
273
        }
213✔
274
        match self {
214✔
275
            FluentValue::String(s) => s,
207✔
276
            FluentValue::Number(n) => n.as_string(),
5✔
277
            FluentValue::Custom(s) => scope.bundle.intls.stringify_value(s.as_ref()),
2✔
278
            FluentValue::Error => "".into(),
×
279
            FluentValue::None => "".into(),
×
280
        }
281
    }
214✔
282

283
    pub fn into_owned<'a>(&self) -> FluentValue<'a> {
429✔
284
        match self {
429✔
285
            FluentValue::String(str) => FluentValue::String(Cow::from(str.to_string())),
130✔
286
            FluentValue::Number(s) => FluentValue::Number(s.clone()),
273✔
287
            FluentValue::Custom(s) => FluentValue::Custom(s.duplicate()),
13✔
288
            FluentValue::Error => FluentValue::Error,
×
289
            FluentValue::None => FluentValue::None,
13✔
290
        }
291
    }
429✔
292
}
293

294
impl<'source> From<String> for FluentValue<'source> {
295
    fn from(s: String) -> Self {
2,444✔
296
        FluentValue::String(s.into())
2,444✔
297
    }
2,444✔
298
}
299

300
impl<'source> From<&'source String> for FluentValue<'source> {
301
    fn from(s: &'source String) -> Self {
×
302
        FluentValue::String(s.into())
×
303
    }
×
304
}
305

306
impl<'source> From<&'source str> for FluentValue<'source> {
307
    fn from(s: &'source str) -> Self {
2,069✔
308
        FluentValue::String(s.into())
2,069✔
309
    }
2,069✔
310
}
311

312
impl<'source> From<Cow<'source, str>> for FluentValue<'source> {
313
    fn from(s: Cow<'source, str>) -> Self {
429✔
314
        FluentValue::String(s)
429✔
315
    }
429✔
316
}
317

318
impl<'source, T> From<Option<T>> for FluentValue<'source>
319
where
320
    T: Into<FluentValue<'source>>,
321
{
322
    fn from(v: Option<T>) -> Self {
3✔
323
        match v {
3✔
324
            Some(v) => v.into(),
2✔
325
            None => FluentValue::None,
1✔
326
        }
327
    }
3✔
328
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc