436 lines
15 KiB
Rust
436 lines
15 KiB
Rust
//! 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));
|
|
}
|
|
}
|
|
}
|