| 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 |  | 
|---|