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