1 | //! This crate contains a memoizer for internationalization formatters. Often it is |
2 | //! expensive (in terms of performance and memory) to construct a formatter, but then |
3 | //! relatively cheap to run the format operation. |
4 | //! |
5 | //! The [IntlMemoizer] is the main struct that creates a per-locale [IntlLangMemoizer]. |
6 | |
7 | use std::cell::RefCell; |
8 | use std::collections::hash_map::Entry; |
9 | use std::collections::HashMap; |
10 | use std::hash::Hash; |
11 | use std::rc::{Rc, Weak}; |
12 | use unic_langid::LanguageIdentifier; |
13 | |
14 | pub mod concurrent; |
15 | |
16 | /// The trait that needs to be implemented for each intl formatter that needs to be |
17 | /// memoized. |
18 | pub trait Memoizable { |
19 | /// Type of the arguments that are used to construct the formatter. |
20 | type Args: 'static + Eq + Hash + Clone; |
21 | |
22 | /// Type of any errors that can occur during the construction process. |
23 | type Error; |
24 | |
25 | /// Construct a formatter. This maps the [`Self::Args`] type to the actual constructor |
26 | /// for an intl formatter. |
27 | fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error> |
28 | where |
29 | Self: std::marker::Sized; |
30 | } |
31 | |
32 | /// The [`IntlLangMemoizer`] can memoize multiple constructed internationalization |
33 | /// formatters, and their configuration for a single locale. For instance, given "en-US", |
34 | /// a memorizer could retain 3 DateTimeFormat instances, and a PluralRules. |
35 | /// |
36 | /// For memoizing with multiple locales, see [`IntlMemoizer`]. |
37 | /// |
38 | /// # Example |
39 | /// |
40 | /// The code example does the following steps: |
41 | /// |
42 | /// 1. Create a static counter |
43 | /// 2. Create an `ExampleFormatter` |
44 | /// 3. Implement [`Memoizable`] for `ExampleFormatter`. |
45 | /// 4. Use `IntlLangMemoizer::with_try_get` to run `ExampleFormatter::format` |
46 | /// 5. Demonstrate the memoization using the static counter |
47 | /// |
48 | /// ``` |
49 | /// use intl_memoizer::{IntlLangMemoizer, Memoizable}; |
50 | /// use unic_langid::LanguageIdentifier; |
51 | /// |
52 | /// // Create a static counter so that we can demonstrate the side effects of when |
53 | /// // the memoizer re-constructs an API. |
54 | /// |
55 | /// static mut INTL_EXAMPLE_CONSTRUCTS: u32 = 0; |
56 | /// fn increment_constructs() { |
57 | /// unsafe { |
58 | /// INTL_EXAMPLE_CONSTRUCTS += 1; |
59 | /// } |
60 | /// } |
61 | /// |
62 | /// fn get_constructs_count() -> u32 { |
63 | /// unsafe { INTL_EXAMPLE_CONSTRUCTS } |
64 | /// } |
65 | /// |
66 | /// /// Create an example formatter, that doesn't really do anything useful. In a real |
67 | /// /// implementation, this could be a PluralRules or DateTimeFormat struct. |
68 | /// struct ExampleFormatter { |
69 | /// lang: LanguageIdentifier, |
70 | /// /// This is here to show how to initiate the API with an argument. |
71 | /// prefix: String, |
72 | /// } |
73 | /// |
74 | /// impl ExampleFormatter { |
75 | /// /// Perform an example format by printing information about the formatter |
76 | /// /// configuration, and the arguments passed into the individual format operation. |
77 | /// fn format(&self, example_string: &str) -> String { |
78 | /// format!( |
79 | /// "{} lang({}) string({})" , |
80 | /// self.prefix, self.lang, example_string |
81 | /// ) |
82 | /// } |
83 | /// } |
84 | /// |
85 | /// /// Multiple classes of structs may be add1ed to the memoizer, with the restriction |
86 | /// /// that they must implement the `Memoizable` trait. |
87 | /// impl Memoizable for ExampleFormatter { |
88 | /// /// The arguments will be passed into the constructor. Here a single `String` |
89 | /// /// will be used as a prefix to the formatting operation. |
90 | /// type Args = (String,); |
91 | /// |
92 | /// /// If the constructor is fallible, than errors can be described here. |
93 | /// type Error = (); |
94 | /// |
95 | /// /// This function wires together the `Args` and `Error` type to construct |
96 | /// /// the intl API. In our example, there is |
97 | /// fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error> { |
98 | /// // Keep track for example purposes that this was constructed. |
99 | /// increment_constructs(); |
100 | /// |
101 | /// Ok(Self { |
102 | /// lang, |
103 | /// prefix: args.0, |
104 | /// }) |
105 | /// } |
106 | /// } |
107 | /// |
108 | /// // The following demonstrates how these structs are actually used with the memoizer. |
109 | /// |
110 | /// // Construct a new memoizer. |
111 | /// let lang = "en-US" .parse().expect("Failed to parse." ); |
112 | /// let memoizer = IntlLangMemoizer::new(lang); |
113 | /// |
114 | /// // These arguments are passed into the constructor for `ExampleFormatter`. |
115 | /// let construct_args = (String::from("prefix:" ),); |
116 | /// let message1 = "The format operation will run" ; |
117 | /// let message2 = "ExampleFormatter will be re-used, when a second format is run" ; |
118 | /// |
119 | /// // Run `IntlLangMemoizer::with_try_get`. The name of the method means "with" an |
120 | /// // intl formatter, "try and get" the result. See the method documentation for |
121 | /// // more details. |
122 | /// |
123 | /// let result1 = memoizer |
124 | /// .with_try_get::<ExampleFormatter, _, _>(construct_args.clone(), |intl_example| { |
125 | /// intl_example.format(message1) |
126 | /// }); |
127 | /// |
128 | /// // The memoized instance of `ExampleFormatter` will be re-used. |
129 | /// let result2 = memoizer |
130 | /// .with_try_get::<ExampleFormatter, _, _>(construct_args.clone(), |intl_example| { |
131 | /// intl_example.format(message2) |
132 | /// }); |
133 | /// |
134 | /// assert_eq!( |
135 | /// result1.unwrap(), |
136 | /// "prefix: lang(en-US) string(The format operation will run)" |
137 | /// ); |
138 | /// assert_eq!( |
139 | /// result2.unwrap(), |
140 | /// "prefix: lang(en-US) string(ExampleFormatter will be re-used, when a second format is run)" |
141 | /// ); |
142 | /// assert_eq!( |
143 | /// get_constructs_count(), |
144 | /// 1, |
145 | /// "The constructor was only run once." |
146 | /// ); |
147 | /// |
148 | /// let construct_args = (String::from("re-init:" ),); |
149 | /// |
150 | /// // Since the constructor args changed, `ExampleFormatter` will be re-constructed. |
151 | /// let result1 = memoizer |
152 | /// .with_try_get::<ExampleFormatter, _, _>(construct_args.clone(), |intl_example| { |
153 | /// intl_example.format(message1) |
154 | /// }); |
155 | /// |
156 | /// // The memoized instance of `ExampleFormatter` will be re-used. |
157 | /// let result2 = memoizer |
158 | /// .with_try_get::<ExampleFormatter, _, _>(construct_args.clone(), |intl_example| { |
159 | /// intl_example.format(message2) |
160 | /// }); |
161 | /// |
162 | /// assert_eq!( |
163 | /// result1.unwrap(), |
164 | /// "re-init: lang(en-US) string(The format operation will run)" |
165 | /// ); |
166 | /// assert_eq!( |
167 | /// result2.unwrap(), |
168 | /// "re-init: lang(en-US) string(ExampleFormatter will be re-used, when a second format is run)" |
169 | /// ); |
170 | /// assert_eq!( |
171 | /// get_constructs_count(), |
172 | /// 2, |
173 | /// "The constructor was invalidated and ran again." |
174 | /// ); |
175 | /// ``` |
176 | #[derive (Debug)] |
177 | pub struct IntlLangMemoizer { |
178 | lang: LanguageIdentifier, |
179 | map: RefCell<type_map::TypeMap>, |
180 | } |
181 | |
182 | impl IntlLangMemoizer { |
183 | /// Create a new [`IntlLangMemoizer`] that is unique to a specific |
184 | /// [`LanguageIdentifier`] |
185 | pub fn new(lang: LanguageIdentifier) -> Self { |
186 | Self { |
187 | lang, |
188 | map: RefCell::new(type_map::TypeMap::new()), |
189 | } |
190 | } |
191 | |
192 | /// `with_try_get` means `with` an internationalization formatter, `try` and `get` a result. |
193 | /// The (potentially expensive) constructor for the formatter (such as PluralRules or |
194 | /// DateTimeFormat) will be memoized and only constructed once for a given |
195 | /// `construct_args`. After that the format operation can be run multiple times |
196 | /// inexpensively. |
197 | /// |
198 | /// The first generic argument `I` must be provided, but the `R` and `U` will be |
199 | /// deduced by the typing of the `callback` argument that is provided. |
200 | /// |
201 | /// I - The memoizable intl object, for instance a `PluralRules` instance. This |
202 | /// must implement the Memoizable trait. |
203 | /// |
204 | /// R - The return result from the callback `U`. |
205 | /// |
206 | /// U - The callback function. Takes an instance of `I` as the first parameter and |
207 | /// returns the R value. |
208 | pub fn with_try_get<I, R, U>(&self, construct_args: I::Args, callback: U) -> Result<R, I::Error> |
209 | where |
210 | Self: Sized, |
211 | I: Memoizable + 'static, |
212 | U: FnOnce(&I) -> R, |
213 | { |
214 | let mut map = self |
215 | .map |
216 | .try_borrow_mut() |
217 | .expect("Cannot use memoizer reentrantly" ); |
218 | let cache = map |
219 | .entry::<HashMap<I::Args, I>>() |
220 | .or_insert_with(HashMap::new); |
221 | |
222 | let e = match cache.entry(construct_args.clone()) { |
223 | Entry::Occupied(entry) => entry.into_mut(), |
224 | Entry::Vacant(entry) => { |
225 | let val = I::construct(self.lang.clone(), construct_args)?; |
226 | entry.insert(val) |
227 | } |
228 | }; |
229 | Ok(callback(e)) |
230 | } |
231 | } |
232 | |
233 | /// [`IntlMemoizer`] is designed to handle lazily-initialized references to |
234 | /// internationalization formatters. |
235 | /// |
236 | /// Constructing a new formatter is often expensive in terms of memory and performance, |
237 | /// and the instance is often read-only during its lifetime. The format operations in |
238 | /// comparison are relatively cheap. |
239 | /// |
240 | /// Because of this relationship, it can be helpful to memoize the constructors, and |
241 | /// re-use them across multiple format operations. This strategy is used where all |
242 | /// instances of intl APIs such as `PluralRules`, `DateTimeFormat` etc. are memoized |
243 | /// between all `FluentBundle` instances. |
244 | /// |
245 | /// # Example |
246 | /// |
247 | /// For a more complete example of the memoization, see the [`IntlLangMemoizer`] documentation. |
248 | /// This example provides a higher-level overview. |
249 | /// |
250 | /// ``` |
251 | /// # use intl_memoizer::{IntlMemoizer, IntlLangMemoizer, Memoizable}; |
252 | /// # use unic_langid::LanguageIdentifier; |
253 | /// # use std::rc::Rc; |
254 | /// # |
255 | /// # struct ExampleFormatter { |
256 | /// # lang: LanguageIdentifier, |
257 | /// # prefix: String, |
258 | /// # } |
259 | /// # |
260 | /// # impl ExampleFormatter { |
261 | /// # fn format(&self, example_string: &str) -> String { |
262 | /// # format!( |
263 | /// # "{} lang({}) string({})" , |
264 | /// # self.prefix, self.lang, example_string |
265 | /// # ) |
266 | /// # } |
267 | /// # } |
268 | /// # |
269 | /// # impl Memoizable for ExampleFormatter { |
270 | /// # type Args = (String,); |
271 | /// # type Error = (); |
272 | /// # fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error> { |
273 | /// # Ok(Self { |
274 | /// # lang, |
275 | /// # prefix: args.0, |
276 | /// # }) |
277 | /// # } |
278 | /// # } |
279 | /// # |
280 | /// let mut memoizer = IntlMemoizer::default(); |
281 | /// |
282 | /// // The memoziation happens per-locale. |
283 | /// let en_us = "en-US" .parse().expect("Failed to parse." ); |
284 | /// let en_us_memoizer: Rc<IntlLangMemoizer> = memoizer.get_for_lang(en_us); |
285 | /// |
286 | /// // These arguments are passed into the constructor for `ExampleFormatter`. The |
287 | /// // construct_args will be used for determining the memoization, but the message |
288 | /// // can be different and re-use the constructed instance. |
289 | /// let construct_args = (String::from("prefix:" ),); |
290 | /// let message = "The format operation will run" ; |
291 | /// |
292 | /// // Use the `ExampleFormatter` from the `IntlLangMemoizer` example. It returns a |
293 | /// // string that demonstrates the configuration of the formatter. This step will |
294 | /// // construct a new formatter if needed, and run the format operation. |
295 | /// // |
296 | /// // See `IntlLangMemoizer` for more details on this step. |
297 | /// let en_us_result = en_us_memoizer |
298 | /// .with_try_get::<ExampleFormatter, _, _>(construct_args.clone(), |intl_example| { |
299 | /// intl_example.format(message) |
300 | /// }); |
301 | /// |
302 | /// // The example formatter constructs a string with diagnostic information about |
303 | /// // the configuration. |
304 | /// assert_eq!( |
305 | /// en_us_result.unwrap(), |
306 | /// "prefix: lang(en-US) string(The format operation will run)" |
307 | /// ); |
308 | /// |
309 | /// // The process can be repeated for a new locale. |
310 | /// |
311 | /// let de_de = "de-DE" .parse().expect("Failed to parse." ); |
312 | /// let de_de_memoizer: Rc<IntlLangMemoizer> = memoizer.get_for_lang(de_de); |
313 | /// |
314 | /// let de_de_result = de_de_memoizer |
315 | /// .with_try_get::<ExampleFormatter, _, _>(construct_args.clone(), |intl_example| { |
316 | /// intl_example.format(message) |
317 | /// }); |
318 | /// |
319 | /// assert_eq!( |
320 | /// de_de_result.unwrap(), |
321 | /// "prefix: lang(de-DE) string(The format operation will run)" |
322 | /// ); |
323 | /// ``` |
324 | #[derive (Default)] |
325 | pub struct IntlMemoizer { |
326 | map: HashMap<LanguageIdentifier, Weak<IntlLangMemoizer>>, |
327 | } |
328 | |
329 | impl IntlMemoizer { |
330 | /// Get a [`IntlLangMemoizer`] for a given language. If one does not exist for |
331 | /// a locale, it will be constructed and weakly retained. See [`IntlLangMemoizer`] |
332 | /// for more detailed documentation how to use it. |
333 | pub fn get_for_lang(&mut self, lang: LanguageIdentifier) -> Rc<IntlLangMemoizer> { |
334 | match self.map.entry(key:lang.clone()) { |
335 | Entry::Vacant(empty: VacantEntry<'_, LanguageIdentifier, …>) => { |
336 | let entry: Rc = Rc::new(IntlLangMemoizer::new(lang)); |
337 | empty.insert(Rc::downgrade(&entry)); |
338 | entry |
339 | } |
340 | Entry::Occupied(mut entry: OccupiedEntry<'_, LanguageIdentifier, …>) => { |
341 | if let Some(entry: Rc) = entry.get().upgrade() { |
342 | entry |
343 | } else { |
344 | let e: Rc = Rc::new(IntlLangMemoizer::new(lang)); |
345 | entry.insert(Rc::downgrade(&e)); |
346 | e |
347 | } |
348 | } |
349 | } |
350 | } |
351 | } |
352 | |
353 | #[cfg (test)] |
354 | mod tests { |
355 | use super::*; |
356 | use fluent_langneg::{negotiate_languages, NegotiationStrategy}; |
357 | use intl_pluralrules::{PluralCategory, PluralRuleType, PluralRules as IntlPluralRules}; |
358 | use std::{sync::Arc, thread}; |
359 | |
360 | struct PluralRules(pub IntlPluralRules); |
361 | |
362 | impl PluralRules { |
363 | pub fn new( |
364 | lang: LanguageIdentifier, |
365 | pr_type: PluralRuleType, |
366 | ) -> Result<Self, &'static str> { |
367 | let default_lang: LanguageIdentifier = "en" .parse().unwrap(); |
368 | let pr_lang = negotiate_languages( |
369 | &[lang], |
370 | &IntlPluralRules::get_locales(pr_type), |
371 | Some(&default_lang), |
372 | NegotiationStrategy::Lookup, |
373 | )[0] |
374 | .clone(); |
375 | |
376 | Ok(Self(IntlPluralRules::create(pr_lang, pr_type)?)) |
377 | } |
378 | } |
379 | |
380 | impl Memoizable for PluralRules { |
381 | type Args = (PluralRuleType,); |
382 | type Error = &'static str; |
383 | fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error> { |
384 | Self::new(lang, args.0) |
385 | } |
386 | } |
387 | |
388 | #[test ] |
389 | fn test_single_thread() { |
390 | let lang: LanguageIdentifier = "en" .parse().unwrap(); |
391 | |
392 | let mut memoizer = IntlMemoizer::default(); |
393 | { |
394 | let en_memoizer = memoizer.get_for_lang(lang.clone()); |
395 | |
396 | let result = en_memoizer |
397 | .with_try_get::<PluralRules, _, _>((PluralRuleType::CARDINAL,), |cb| cb.0.select(5)) |
398 | .unwrap(); |
399 | assert_eq!(result, Ok(PluralCategory::OTHER)); |
400 | } |
401 | |
402 | { |
403 | let en_memoizer = memoizer.get_for_lang(lang); |
404 | |
405 | let result = en_memoizer |
406 | .with_try_get::<PluralRules, _, _>((PluralRuleType::CARDINAL,), |cb| cb.0.select(5)) |
407 | .unwrap(); |
408 | assert_eq!(result, Ok(PluralCategory::OTHER)); |
409 | } |
410 | } |
411 | |
412 | #[test ] |
413 | fn test_concurrent() { |
414 | let lang: LanguageIdentifier = "en" .parse().unwrap(); |
415 | let memoizer = Arc::new(concurrent::IntlLangMemoizer::new(lang)); |
416 | let mut threads = vec![]; |
417 | |
418 | // Spawn four threads that all use the PluralRules. |
419 | for _ in 0..4 { |
420 | let memoizer = Arc::clone(&memoizer); |
421 | threads.push(thread::spawn(move || { |
422 | memoizer |
423 | .with_try_get::<PluralRules, _, _>((PluralRuleType::CARDINAL,), |cb| { |
424 | cb.0.select(5) |
425 | }) |
426 | .expect("Failed to get a PluralRules result." ) |
427 | })); |
428 | } |
429 | |
430 | for thread in threads.drain(..) { |
431 | let result = thread.join().expect("Failed to join thread." ); |
432 | assert_eq!(result, Ok(PluralCategory::OTHER)); |
433 | } |
434 | } |
435 | } |
436 | |