blob: a4604e1a51e1780ac0553e53c2b6b52dcdd9fe6c [file] [log] [blame] [edit]
//! This crate contains a memoizer for internationalization formatters. Often it is
//! expensive (in terms of performance and memory) to construct a formatter, but then
//! relatively cheap to run the format operation.
//!
//! The [IntlMemoizer] is the main struct that creates a per-locale [IntlLangMemoizer].
use std::cell::RefCell;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::hash::Hash;
use std::rc::{Rc, Weak};
use unic_langid::LanguageIdentifier;
pub mod concurrent;
/// The trait that needs to be implemented for each intl formatter that needs to be
/// memoized.
pub trait Memoizable {
/// Type of the arguments that are used to construct the formatter.
type Args: 'static + Eq + Hash + Clone;
/// Type of any errors that can occur during the construction process.
type Error;
/// Construct a formatter. This maps the [`Self::Args`] type to the actual constructor
/// for an intl formatter.
fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error>
where
Self: std::marker::Sized;
}
/// The [`IntlLangMemoizer`] can memoize multiple constructed internationalization
/// formatters, and their configuration for a single locale. For instance, given "en-US",
/// a memorizer could retain 3 DateTimeFormat instances, and a PluralRules.
///
/// For memoizing with multiple locales, see [`IntlMemoizer`].
///
/// # Example
///
/// The code example does the following steps:
///
/// 1. Create a static counter
/// 2. Create an `ExampleFormatter`
/// 3. Implement [`Memoizable`] for `ExampleFormatter`.
/// 4. Use `IntlLangMemoizer::with_try_get` to run `ExampleFormatter::format`
/// 5. Demonstrate the memoization using the static counter
///
/// ```
/// use intl_memoizer::{IntlLangMemoizer, Memoizable};
/// use unic_langid::LanguageIdentifier;
///
/// // Create a static counter so that we can demonstrate the side effects of when
/// // the memoizer re-constructs an API.
///
/// static mut INTL_EXAMPLE_CONSTRUCTS: u32 = 0;
/// fn increment_constructs() {
/// unsafe {
/// INTL_EXAMPLE_CONSTRUCTS += 1;
/// }
/// }
///
/// fn get_constructs_count() -> u32 {
/// unsafe { INTL_EXAMPLE_CONSTRUCTS }
/// }
///
/// /// Create an example formatter, that doesn't really do anything useful. In a real
/// /// implementation, this could be a PluralRules or DateTimeFormat struct.
/// struct ExampleFormatter {
/// lang: LanguageIdentifier,
/// /// This is here to show how to initiate the API with an argument.
/// prefix: String,
/// }
///
/// impl ExampleFormatter {
/// /// Perform an example format by printing information about the formatter
/// /// configuration, and the arguments passed into the individual format operation.
/// fn format(&self, example_string: &str) -> String {
/// format!(
/// "{} lang({}) string({})",
/// self.prefix, self.lang, example_string
/// )
/// }
/// }
///
/// /// Multiple classes of structs may be add1ed to the memoizer, with the restriction
/// /// that they must implement the `Memoizable` trait.
/// impl Memoizable for ExampleFormatter {
/// /// The arguments will be passed into the constructor. Here a single `String`
/// /// will be used as a prefix to the formatting operation.
/// type Args = (String,);
///
/// /// If the constructor is fallible, than errors can be described here.
/// type Error = ();
///
/// /// This function wires together the `Args` and `Error` type to construct
/// /// the intl API. In our example, there is
/// fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error> {
/// // Keep track for example purposes that this was constructed.
/// increment_constructs();
///
/// Ok(Self {
/// lang,
/// prefix: args.0,
/// })
/// }
/// }
///
/// // The following demonstrates how these structs are actually used with the memoizer.
///
/// // Construct a new memoizer.
/// let lang = "en-US".parse().expect("Failed to parse.");
/// let memoizer = IntlLangMemoizer::new(lang);
///
/// // These arguments are passed into the constructor for `ExampleFormatter`.
/// let construct_args = (String::from("prefix:"),);
/// let message1 = "The format operation will run";
/// let message2 = "ExampleFormatter will be re-used, when a second format is run";
///
/// // Run `IntlLangMemoizer::with_try_get`. The name of the method means "with" an
/// // intl formatter, "try and get" the result. See the method documentation for
/// // more details.
///
/// let result1 = memoizer
/// .with_try_get::<ExampleFormatter, _, _>(construct_args.clone(), |intl_example| {
/// intl_example.format(message1)
/// });
///
/// // The memoized instance of `ExampleFormatter` will be re-used.
/// let result2 = memoizer
/// .with_try_get::<ExampleFormatter, _, _>(construct_args.clone(), |intl_example| {
/// intl_example.format(message2)
/// });
///
/// assert_eq!(
/// result1.unwrap(),
/// "prefix: lang(en-US) string(The format operation will run)"
/// );
/// assert_eq!(
/// result2.unwrap(),
/// "prefix: lang(en-US) string(ExampleFormatter will be re-used, when a second format is run)"
/// );
/// assert_eq!(
/// get_constructs_count(),
/// 1,
/// "The constructor was only run once."
/// );
///
/// let construct_args = (String::from("re-init:"),);
///
/// // Since the constructor args changed, `ExampleFormatter` will be re-constructed.
/// let result1 = memoizer
/// .with_try_get::<ExampleFormatter, _, _>(construct_args.clone(), |intl_example| {
/// intl_example.format(message1)
/// });
///
/// // The memoized instance of `ExampleFormatter` will be re-used.
/// let result2 = memoizer
/// .with_try_get::<ExampleFormatter, _, _>(construct_args.clone(), |intl_example| {
/// intl_example.format(message2)
/// });
///
/// assert_eq!(
/// result1.unwrap(),
/// "re-init: lang(en-US) string(The format operation will run)"
/// );
/// assert_eq!(
/// result2.unwrap(),
/// "re-init: lang(en-US) string(ExampleFormatter will be re-used, when a second format is run)"
/// );
/// assert_eq!(
/// get_constructs_count(),
/// 2,
/// "The constructor was invalidated and ran again."
/// );
/// ```
#[derive(Debug)]
pub struct IntlLangMemoizer {
lang: LanguageIdentifier,
map: RefCell<type_map::TypeMap>,
}
impl IntlLangMemoizer {
/// Create a new [`IntlLangMemoizer`] that is unique to a specific
/// [`LanguageIdentifier`]
pub fn new(lang: LanguageIdentifier) -> Self {
Self {
lang,
map: RefCell::new(type_map::TypeMap::new()),
}
}
/// `with_try_get` means `with` an internationalization formatter, `try` and `get` a result.
/// The (potentially expensive) constructor for the formatter (such as PluralRules or
/// DateTimeFormat) will be memoized and only constructed once for a given
/// `construct_args`. After that the format operation can be run multiple times
/// inexpensively.
///
/// The first generic argument `I` must be provided, but the `R` and `U` will be
/// deduced by the typing of the `callback` argument that is provided.
///
/// I - The memoizable intl object, for instance a `PluralRules` instance. This
/// must implement the Memoizable trait.
///
/// R - The return result from the callback `U`.
///
/// U - The callback function. Takes an instance of `I` as the first parameter and
/// returns the R value.
pub fn with_try_get<I, R, U>(&self, construct_args: I::Args, callback: U) -> Result<R, I::Error>
where
Self: Sized,
I: Memoizable + 'static,
U: FnOnce(&I) -> R,
{
let mut map = self
.map
.try_borrow_mut()
.expect("Cannot use memoizer reentrantly");
let cache = map
.entry::<HashMap<I::Args, I>>()
.or_insert_with(HashMap::new);
let e = match cache.entry(construct_args.clone()) {
Entry::Occupied(entry) => entry.into_mut(),
Entry::Vacant(entry) => {
let val = I::construct(self.lang.clone(), construct_args)?;
entry.insert(val)
}
};
Ok(callback(e))
}
}
/// [`IntlMemoizer`] is designed to handle lazily-initialized references to
/// internationalization formatters.
///
/// Constructing a new formatter is often expensive in terms of memory and performance,
/// and the instance is often read-only during its lifetime. The format operations in
/// comparison are relatively cheap.
///
/// Because of this relationship, it can be helpful to memoize the constructors, and
/// re-use them across multiple format operations. This strategy is used where all
/// instances of intl APIs such as `PluralRules`, `DateTimeFormat` etc. are memoized
/// between all `FluentBundle` instances.
///
/// # Example
///
/// For a more complete example of the memoization, see the [`IntlLangMemoizer`] documentation.
/// This example provides a higher-level overview.
///
/// ```
/// # use intl_memoizer::{IntlMemoizer, IntlLangMemoizer, Memoizable};
/// # use unic_langid::LanguageIdentifier;
/// # use std::rc::Rc;
/// #
/// # struct ExampleFormatter {
/// # lang: LanguageIdentifier,
/// # prefix: String,
/// # }
/// #
/// # impl ExampleFormatter {
/// # fn format(&self, example_string: &str) -> String {
/// # format!(
/// # "{} lang({}) string({})",
/// # self.prefix, self.lang, example_string
/// # )
/// # }
/// # }
/// #
/// # impl Memoizable for ExampleFormatter {
/// # type Args = (String,);
/// # type Error = ();
/// # fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error> {
/// # Ok(Self {
/// # lang,
/// # prefix: args.0,
/// # })
/// # }
/// # }
/// #
/// let mut memoizer = IntlMemoizer::default();
///
/// // The memoziation happens per-locale.
/// let en_us = "en-US".parse().expect("Failed to parse.");
/// let en_us_memoizer: Rc<IntlLangMemoizer> = memoizer.get_for_lang(en_us);
///
/// // These arguments are passed into the constructor for `ExampleFormatter`. The
/// // construct_args will be used for determining the memoization, but the message
/// // can be different and re-use the constructed instance.
/// let construct_args = (String::from("prefix:"),);
/// let message = "The format operation will run";
///
/// // Use the `ExampleFormatter` from the `IntlLangMemoizer` example. It returns a
/// // string that demonstrates the configuration of the formatter. This step will
/// // construct a new formatter if needed, and run the format operation.
/// //
/// // See `IntlLangMemoizer` for more details on this step.
/// let en_us_result = en_us_memoizer
/// .with_try_get::<ExampleFormatter, _, _>(construct_args.clone(), |intl_example| {
/// intl_example.format(message)
/// });
///
/// // The example formatter constructs a string with diagnostic information about
/// // the configuration.
/// assert_eq!(
/// en_us_result.unwrap(),
/// "prefix: lang(en-US) string(The format operation will run)"
/// );
///
/// // The process can be repeated for a new locale.
///
/// let de_de = "de-DE".parse().expect("Failed to parse.");
/// let de_de_memoizer: Rc<IntlLangMemoizer> = memoizer.get_for_lang(de_de);
///
/// let de_de_result = de_de_memoizer
/// .with_try_get::<ExampleFormatter, _, _>(construct_args.clone(), |intl_example| {
/// intl_example.format(message)
/// });
///
/// assert_eq!(
/// de_de_result.unwrap(),
/// "prefix: lang(de-DE) string(The format operation will run)"
/// );
/// ```
#[derive(Default)]
pub struct IntlMemoizer {
map: HashMap<LanguageIdentifier, Weak<IntlLangMemoizer>>,
}
impl IntlMemoizer {
/// Get a [`IntlLangMemoizer`] for a given language. If one does not exist for
/// a locale, it will be constructed and weakly retained. See [`IntlLangMemoizer`]
/// for more detailed documentation how to use it.
pub fn get_for_lang(&mut self, lang: LanguageIdentifier) -> Rc<IntlLangMemoizer> {
match self.map.entry(lang.clone()) {
Entry::Vacant(empty) => {
let entry = Rc::new(IntlLangMemoizer::new(lang));
empty.insert(Rc::downgrade(&entry));
entry
}
Entry::Occupied(mut entry) => {
if let Some(entry) = entry.get().upgrade() {
entry
} else {
let e = Rc::new(IntlLangMemoizer::new(lang));
entry.insert(Rc::downgrade(&e));
e
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use fluent_langneg::{negotiate_languages, NegotiationStrategy};
use intl_pluralrules::{PluralCategory, PluralRuleType, PluralRules as IntlPluralRules};
use std::{sync::Arc, thread};
struct PluralRules(pub IntlPluralRules);
impl PluralRules {
pub fn new(
lang: LanguageIdentifier,
pr_type: PluralRuleType,
) -> Result<Self, &'static str> {
let default_lang: LanguageIdentifier = "en".parse().unwrap();
let pr_lang = negotiate_languages(
&[lang],
&IntlPluralRules::get_locales(pr_type),
Some(&default_lang),
NegotiationStrategy::Lookup,
)[0]
.clone();
Ok(Self(IntlPluralRules::create(pr_lang, pr_type)?))
}
}
impl Memoizable for PluralRules {
type Args = (PluralRuleType,);
type Error = &'static str;
fn construct(lang: LanguageIdentifier, args: Self::Args) -> Result<Self, Self::Error> {
Self::new(lang, args.0)
}
}
#[test]
fn test_single_thread() {
let lang: LanguageIdentifier = "en".parse().unwrap();
let mut memoizer = IntlMemoizer::default();
{
let en_memoizer = memoizer.get_for_lang(lang.clone());
let result = en_memoizer
.with_try_get::<PluralRules, _, _>((PluralRuleType::CARDINAL,), |cb| cb.0.select(5))
.unwrap();
assert_eq!(result, Ok(PluralCategory::OTHER));
}
{
let en_memoizer = memoizer.get_for_lang(lang);
let result = en_memoizer
.with_try_get::<PluralRules, _, _>((PluralRuleType::CARDINAL,), |cb| cb.0.select(5))
.unwrap();
assert_eq!(result, Ok(PluralCategory::OTHER));
}
}
#[test]
fn test_concurrent() {
let lang: LanguageIdentifier = "en".parse().unwrap();
let memoizer = Arc::new(concurrent::IntlLangMemoizer::new(lang));
let mut threads = vec![];
// Spawn four threads that all use the PluralRules.
for _ in 0..4 {
let memoizer = Arc::clone(&memoizer);
threads.push(thread::spawn(move || {
memoizer
.with_try_get::<PluralRules, _, _>((PluralRuleType::CARDINAL,), |cb| {
cb.0.select(5)
})
.expect("Failed to get a PluralRules result.")
}));
}
for thread in threads.drain(..) {
let result = thread.join().expect("Failed to join thread.");
assert_eq!(result, Ok(PluralCategory::OTHER));
}
}
}