blob: 8d9d79f2da068947d3ce44f597625b634388cadd [file] [log] [blame]
// This file is part of ICU4X. For terms of use, please see the file
// called LICENSE at the top level of the ICU4X source tree
// (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
use super::*;
use crate::helpers::result_is_err_missing_locale;
/// A data provider wrapper that performs locale fallback. This enables arbitrary locales to be
/// handled at runtime.
///
/// # Examples
///
/// ```
/// use icu_locid::locale;
/// use icu_provider::prelude::*;
/// use icu_provider::hello_world::*;
/// use icu_provider_adapters::fallback::LocaleFallbackProvider;
///
/// let provider = icu_testdata::unstable_no_fallback();
///
/// let req = DataRequest {
/// locale: &locale!("ja-JP").into(),
/// metadata: Default::default(),
/// };
///
/// // The provider does not have data for "ja-JP":
/// DataProvider::<HelloWorldV1Marker>::load(&provider, req).expect_err("No fallback");
///
/// // But if we wrap the provider in a fallback provider...
/// let provider = LocaleFallbackProvider::try_new_unstable(provider)
/// .expect("Fallback data present");
///
/// // ...then we can load "ja-JP" based on "ja" data
/// let response =
/// DataProvider::<HelloWorldV1Marker>::load(&provider, req).expect("successful with vertical fallback");
///
/// assert_eq!(
/// response.metadata.locale.unwrap(),
/// locale!("ja").into(),
/// );
/// assert_eq!(
/// response.payload.unwrap().get().message,
/// "こんにちは世界",
/// );
/// ```
#[derive(Clone, Debug)]
pub struct LocaleFallbackProvider<P> {
inner: P,
fallbacker: LocaleFallbacker,
}
impl<P> LocaleFallbackProvider<P>
where
P: DataProvider<LocaleFallbackLikelySubtagsV1Marker>
+ DataProvider<LocaleFallbackParentsV1Marker>
+ DataProvider<CollationFallbackSupplementV1Marker>,
{
/// Create a [`LocaleFallbackProvider`] by wrapping another data provider and then loading
/// fallback data from it.
///
/// If the data provider being wrapped does not contain fallback data, use
/// [`LocaleFallbackProvider::new_with_fallbacker`].
pub fn try_new_unstable(provider: P) -> Result<Self, DataError> {
let fallbacker = LocaleFallbacker::try_new_unstable(&provider)?;
Ok(Self {
inner: provider,
fallbacker,
})
}
}
impl<P> LocaleFallbackProvider<P>
where
P: AnyProvider,
{
/// Create a [`LocaleFallbackProvider`] by wrapping another data provider and then loading
/// fallback data from it.
///
/// If the data provider being wrapped does not contain fallback data, use
/// [`LocaleFallbackProvider::new_with_fallbacker`].
pub fn try_new_with_any_provider(provider: P) -> Result<Self, DataError> {
let fallbacker = LocaleFallbacker::try_new_with_any_provider(&provider)?;
Ok(Self {
inner: provider,
fallbacker,
})
}
}
#[cfg(feature = "serde")]
impl<P> LocaleFallbackProvider<P>
where
P: BufferProvider,
{
/// Create a [`LocaleFallbackProvider`] by wrapping another data provider and then loading
/// fallback data from it.
///
/// If the data provider being wrapped does not contain fallback data, use
/// [`LocaleFallbackProvider::new_with_fallbacker`].
pub fn try_new_with_buffer_provider(provider: P) -> Result<Self, DataError> {
let fallbacker = LocaleFallbacker::try_new_with_buffer_provider(&provider)?;
Ok(Self {
inner: provider,
fallbacker,
})
}
}
impl<P> LocaleFallbackProvider<P> {
/// Wrap a provider with an arbitrary fallback engine.
///
/// This relaxes the requirement that the wrapped provider contains its own fallback data.
///
/// # Examples
///
/// ```
/// use icu_locid::locale;
/// use icu_provider::hello_world::*;
/// use icu_provider::prelude::*;
/// use icu_provider_adapters::fallback::{
/// LocaleFallbackProvider, LocaleFallbacker,
/// };
///
/// let provider = HelloWorldProvider;
///
/// let req = DataRequest {
/// locale: &locale!("de-CH").into(),
/// metadata: Default::default(),
/// };
///
/// // There is no "de-CH" data in the `HelloWorldProvider`
/// DataProvider::<HelloWorldV1Marker>::load(&provider, req)
/// .expect_err("No data for de-CH");
///
/// // `HelloWorldProvider` does not contain fallback data,
/// // but we can fetch it from `icu_testdata`, and then
/// // use it to create the fallbacking data provider.
/// let fallbacker =
/// LocaleFallbacker::try_new_unstable(&icu_testdata::unstable())
/// .expect("Fallback data present");
/// let provider =
/// LocaleFallbackProvider::new_with_fallbacker(provider, fallbacker);
///
/// // Now we can load the "de-CH" data via fallback to "de".
/// let german_hello_world: DataPayload<HelloWorldV1Marker> = provider
/// .load(req)
/// .expect("Loading should succeed")
/// .take_payload()
/// .expect("Data should be present");
///
/// assert_eq!("Hallo Welt", german_hello_world.get().message);
/// ```
pub fn new_with_fallbacker(provider: P, fallbacker: LocaleFallbacker) -> Self {
Self {
inner: provider,
fallbacker,
}
}
/// Returns a reference to the inner provider, bypassing fallback.
pub fn inner(&self) -> &P {
&self.inner
}
/// Returns a mutable reference to the inner provider.
pub fn inner_mut(&mut self) -> &mut P {
&mut self.inner
}
/// Returns ownership of the inner provider to the caller.
pub fn into_inner(self) -> P {
self.inner
}
/// Run the fallback algorithm with the data request using the inner data provider.
/// Internal function; external clients should use one of the trait impls below.
///
/// Function arguments:
///
/// - F1 should perform a data load for a single DataRequest and return the result of it
/// - F2 should map from the provider-specific response type to DataResponseMetadata
fn run_fallback<F1, F2, R>(
&self,
key: DataKey,
mut base_req: DataRequest,
mut f1: F1,
mut f2: F2,
) -> Result<R, DataError>
where
F1: FnMut(DataRequest) -> Result<R, DataError>,
F2: FnMut(&mut R) -> &mut DataResponseMetadata,
{
let key_fallbacker = self.fallbacker.for_key(key);
let mut fallback_iterator = key_fallbacker.fallback_for(base_req.locale.clone());
let base_silent = core::mem::replace(&mut base_req.metadata.silent, true);
loop {
let result = f1(DataRequest {
locale: fallback_iterator.get(),
metadata: base_req.metadata,
});
if !result_is_err_missing_locale(&result) {
return result
.map(|mut res| {
f2(&mut res).locale = Some(fallback_iterator.take());
res
})
// Log the original request rather than the fallback request
.map_err(|e| {
base_req.metadata.silent = base_silent;
e.with_req(key, base_req)
});
}
// If we just checked und, break out of the loop.
if fallback_iterator.get().is_empty() {
break;
}
fallback_iterator.step();
}
base_req.metadata.silent = base_silent;
Err(DataErrorKind::MissingLocale.with_req(key, base_req))
}
}
impl<P> AnyProvider for LocaleFallbackProvider<P>
where
P: AnyProvider,
{
fn load_any(&self, key: DataKey, base_req: DataRequest) -> Result<AnyResponse, DataError> {
self.run_fallback(
key,
base_req,
|req| self.inner.load_any(key, req),
|res| &mut res.metadata,
)
}
}
impl<P> BufferProvider for LocaleFallbackProvider<P>
where
P: BufferProvider,
{
fn load_buffer(
&self,
key: DataKey,
base_req: DataRequest,
) -> Result<DataResponse<BufferMarker>, DataError> {
self.run_fallback(
key,
base_req,
|req| self.inner.load_buffer(key, req),
|res| &mut res.metadata,
)
}
}
impl<P, M> DynamicDataProvider<M> for LocaleFallbackProvider<P>
where
P: DynamicDataProvider<M>,
M: DataMarker,
{
fn load_data(&self, key: DataKey, base_req: DataRequest) -> Result<DataResponse<M>, DataError> {
self.run_fallback(
key,
base_req,
|req| self.inner.load_data(key, req),
|res| &mut res.metadata,
)
}
}
impl<P, M> DataProvider<M> for LocaleFallbackProvider<P>
where
P: DataProvider<M>,
M: KeyedDataMarker,
{
fn load(&self, base_req: DataRequest) -> Result<DataResponse<M>, DataError> {
self.run_fallback(
M::KEY,
base_req,
|req| self.inner.load(req),
|res| &mut res.metadata,
)
}
}