From 0f5955a19973ad32121f3d2aa0ca5dfa150a0282 Mon Sep 17 00:00:00 2001 From: Zibi Braniecki Date: Wed, 7 Jul 2021 00:13:40 -0400 Subject: [PATCH] Handle state changes mid async formats --- fluent-fallback/Cargo.toml | 2 +- fluent-fallback/src/localization.rs | 108 +++++++++++++-------- fluent-fallback/tests/localization_test.rs | 48 ++++++++- 3 files changed, 113 insertions(+), 45 deletions(-) diff --git a/fluent-fallback/Cargo.toml b/fluent-fallback/Cargo.toml index 68b011fc..0fa1f3c6 100644 --- a/fluent-fallback/Cargo.toml +++ b/fluent-fallback/Cargo.toml @@ -21,7 +21,6 @@ categories = ["localization", "internationalization"] chunky-vec = "0.1" fluent-bundle = "0.15.1" futures = "0.3" -once_cell = "1.7" async-trait = "0.1" unic-langid = { version = "0.9" } @@ -29,3 +28,4 @@ unic-langid = { version = "0.9" } fluent-langneg = "0.13" unic-langid = { version = "0.9", features = ["macros"] } fluent-resmgr = { path = "../fluent-resmgr" } +tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] } diff --git a/fluent-fallback/src/localization.rs b/fluent-fallback/src/localization.rs index ac8041c1..b73dc3b4 100644 --- a/fluent-fallback/src/localization.rs +++ b/fluent-fallback/src/localization.rs @@ -3,9 +3,11 @@ use crate::env::LocalesProvider; use crate::errors::LocalizationError; use crate::generator::{BundleGenerator, BundleIterator, BundleStream}; use crate::types::{L10nAttribute, L10nKey, L10nMessage}; +use chunky_vec::ChunkyVec; use fluent_bundle::{FluentArgs, FluentBundle, FluentError}; -use once_cell::sync::OnceCell; use std::borrow::Cow; +use std::cell::RefCell; +use std::cell::UnsafeCell; enum Bundles where @@ -43,18 +45,23 @@ where } } +struct State { + dirty: bool, + sync: bool, + res_ids: Vec, +} + pub struct Localization where G: BundleGenerator, P: LocalesProvider, { - // Replace with `OneCell` once it stabilizes - // https://github.com/rust-lang/rust/issues/74465 - bundles: OnceCell>, + // XXX: How to make us Drop the `Bundles` once all callers + // relying on it complete? + bundles: UnsafeCell>>, generator: G, provider: P, - res_ids: Vec, - sync: bool, + state: RefCell, } impl Localization @@ -64,11 +71,14 @@ where { pub fn new(res_ids: Vec, sync: bool) -> Self { Self { - bundles: OnceCell::new(), + bundles: Default::default(), generator: G::default(), provider: P::default(), - res_ids, - sync, + state: RefCell::new(State { + dirty: false, + sync, + res_ids, + }), } } } @@ -80,49 +90,56 @@ where { pub fn with_env(res_ids: Vec, sync: bool, provider: P, generator: G) -> Self { Self { - bundles: OnceCell::new(), + bundles: Default::default(), generator, provider, - res_ids, - sync, + state: RefCell::new(State { + dirty: false, + sync, + res_ids, + }), } } pub fn is_sync(&self) -> bool { - self.sync + self.state.borrow().sync } - pub fn add_resource_id(&mut self, res_id: String) { - self.res_ids.push(res_id); + pub fn add_resource_id(&self, res_id: String) { + RefCell::borrow_mut(&self.state).res_ids.push(res_id); self.on_change(); } - pub fn add_resource_ids(&mut self, res_ids: Vec) { - self.res_ids.extend(res_ids); + pub fn add_resource_ids(&self, res_ids: Vec) { + RefCell::borrow_mut(&self.state).res_ids.extend(res_ids); self.on_change(); } - pub fn remove_resource_id(&mut self, res_id: String) -> usize { - self.res_ids.retain(|x| *x != res_id); + pub fn remove_resource_id(&self, res_id: String) -> usize { + RefCell::borrow_mut(&self.state) + .res_ids + .retain(|x| *x != res_id); self.on_change(); - self.res_ids.len() + RefCell::borrow(&self.state).res_ids.len() } - pub fn remove_resource_ids(&mut self, res_ids: Vec) -> usize { - self.res_ids.retain(|x| !res_ids.contains(x)); + pub fn remove_resource_ids(&self, res_ids: Vec) -> usize { + RefCell::borrow_mut(&self.state) + .res_ids + .retain(|x| !res_ids.contains(x)); self.on_change(); - self.res_ids.len() + RefCell::borrow(&self.state).res_ids.len() } - pub fn set_async(&mut self) { - if self.sync { - self.bundles.take(); - self.sync = false; + pub fn set_async(&self) { + if self.state.borrow().sync { + RefCell::borrow_mut(&self.state).sync = false; + self.on_change(); } } - pub fn on_change(&mut self) { - self.bundles.take(); + pub fn on_change(&self) { + RefCell::borrow_mut(&self.state).dirty = true; } pub async fn format_value<'l>( @@ -228,19 +245,30 @@ where P: LocalesProvider, { fn get_bundles(&self) -> &Bundles { - self.bundles.get_or_init(|| { - if self.sync { - Bundles::Iter(Cache::new( - self.generator - .bundles_iter(self.provider.locales(), self.res_ids.clone()), - )) + unsafe { + let bundles = self.bundles.get(); + if (*bundles).is_empty() || self.state.borrow().dirty { + RefCell::borrow_mut(&self.state).dirty = false; + let state = self.state.borrow(); + let new_iter = { + if state.sync { + Bundles::Iter(Cache::new( + self.generator + .bundles_iter(self.provider.locales(), state.res_ids.clone()), + )) + } else { + Bundles::Stream(AsyncCache::new( + self.generator + .bundles_stream(self.provider.locales(), state.res_ids.clone()), + )) + } + }; + (*bundles).push_get(new_iter) } else { - Bundles::Stream(AsyncCache::new( - self.generator - .bundles_stream(self.provider.locales(), self.res_ids.clone()), - )) + let len = (*bundles).len(); + (*bundles).get(len - 1).unwrap() } - }) + } } fn format_message_from_bundle<'l>( diff --git a/fluent-fallback/tests/localization_test.rs b/fluent-fallback/tests/localization_test.rs index beb58ac2..4dbba833 100644 --- a/fluent-fallback/tests/localization_test.rs +++ b/fluent-fallback/tests/localization_test.rs @@ -93,10 +93,34 @@ impl futures::Stream for BundleIter { type Item = FluentBundleResult; fn poll_next( - self: std::pin::Pin<&mut Self>, + mut self: std::pin::Pin<&mut Self>, _cx: &mut std::task::Context<'_>, ) -> std::task::Poll> { - todo!() + if let Some(locale) = self.locales.next() { + let mut bundle = FluentBundle::new(vec![locale.clone()]); + bundle.set_use_isolating(false); + + let mut errors = vec![]; + for res_id in &self.res_ids { + let full_path = format!("./tests/resources/{}/{}", locale, res_id); + let source = fs::read_to_string(full_path).unwrap(); + let res = match FluentResource::try_new(source) { + Ok(res) => res, + Err((res, err)) => { + errors.extend(err.into_iter().map(Into::into)); + res + } + }; + bundle.add_resource(res).unwrap(); + } + if errors.is_empty() { + Some(Ok(bundle)).into() + } else { + Some(Err((bundle, errors))).into() + } + } else { + None.into() + } } } @@ -112,8 +136,8 @@ impl BundleGenerator for ResourceManager { BundleIter { locales, res_ids } } - fn bundles_stream(&self, _locales: Self::LocalesIter, _res_ids: Vec) -> Self::Stream { - todo!() + fn bundles_stream(&self, locales: Self::LocalesIter, res_ids: Vec) -> Self::Stream { + BundleIter { locales, res_ids } } } @@ -440,3 +464,19 @@ fn localization_format_missing_argument_error() { },] ); } + +#[tokio::test] +async fn localization_handle_state_changes_mid_async() { + let resource_ids: Vec = vec!["test.ftl".into()]; + let locales = Locales::new(vec![langid!("en-US")]); + let res_mgr = ResourceManager; + let mut errors = vec![]; + + let loc = Localization::with_env(resource_ids, false, locales, res_mgr); + + let future = loc.format_value("key", None, &mut errors); + + loc.add_resource_id("test2.ftl".to_string()); + + future.await; +}