• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // This file is part of ICU4X. For terms of use, please see the file
2 // called LICENSE at the top level of the ICU4X source tree
3 // (online at: https://github.com/unicode-org/icu4x/blob/main/LICENSE ).
4 
5 //! A data provider wrapper that performs locale fallback.
6 
7 #[doc(no_inline)]
8 pub use icu_locale::LocaleFallbacker;
9 use icu_provider::prelude::*;
10 use icu_provider::DryDataProvider;
11 use icu_provider::DynamicDryDataProvider;
12 
13 /// A data provider wrapper that performs locale fallback. This enables arbitrary locales to be
14 /// handled at runtime.
15 ///
16 /// # Examples
17 ///
18 /// ```
19 /// use icu_locale::langid;
20 /// use icu_provider::hello_world::*;
21 /// use icu_provider::prelude::*;
22 /// use icu_provider_adapters::fallback::LocaleFallbackProvider;
23 ///
24 /// let provider = HelloWorldProvider;
25 ///
26 /// let id = DataIdentifierCow::from_locale(langid!("ja-JP").into());
27 ///
28 /// // The provider does not have data for "ja-JP":
29 /// DataProvider::<HelloWorldV1>::load(
30 ///     &provider,
31 ///     DataRequest {
32 ///         id: id.as_borrowed(),
33 ///         ..Default::default()
34 ///     },
35 /// )
36 /// .expect_err("No fallback");
37 ///
38 /// // But if we wrap the provider in a fallback provider...
39 /// let provider = LocaleFallbackProvider::new(
40 ///     provider,
41 ///     icu_locale::LocaleFallbacker::new().static_to_owned(),
42 /// );
43 ///
44 /// // ...then we can load "ja-JP" based on "ja" data
45 /// let response = DataProvider::<HelloWorldV1>::load(
46 ///     &provider,
47 ///     DataRequest {
48 ///         id: id.as_borrowed(),
49 ///         ..Default::default()
50 ///     },
51 /// )
52 /// .expect("successful with vertical fallback");
53 ///
54 /// assert_eq!(response.metadata.locale.unwrap(), langid!("ja").into(),);
55 /// assert_eq!(response.payload.get().message, "こんにちは世界",);
56 /// ```
57 #[derive(Clone, Debug)]
58 pub struct LocaleFallbackProvider<P> {
59     inner: P,
60     fallbacker: LocaleFallbacker,
61 }
62 
63 impl<P> LocaleFallbackProvider<P> {
64     /// Wraps a provider with a provider performing fallback under the given fallbacker.
65     ///
66     /// If the underlying provider contains deduplicated data, it is important to use the
67     /// same fallback data that `ExportDriver` used.
68     ///
69     /// # Examples
70     ///
71     /// ```
72     /// use icu_locale::langid;
73     /// use icu_locale::LocaleFallbacker;
74     /// use icu_provider::hello_world::*;
75     /// use icu_provider::prelude::*;
76     /// use icu_provider_adapters::fallback::LocaleFallbackProvider;
77     ///
78     /// let provider = HelloWorldProvider;
79     ///
80     /// let id = DataIdentifierCow::from_locale(langid!("de-CH").into());
81     ///
82     /// // There is no "de-CH" data in the `HelloWorldProvider`
83     /// DataProvider::<HelloWorldV1>::load(
84     ///     &provider,
85     ///     DataRequest {
86     ///         id: id.as_borrowed(),
87     ///         ..Default::default()
88     ///     },
89     /// )
90     /// .expect_err("No data for de-CH");
91     ///
92     /// // `HelloWorldProvider` does not contain fallback data,
93     /// // but we can construct a fallbacker with `icu_locale`'s
94     /// // compiled data.
95     /// let provider = LocaleFallbackProvider::new(
96     ///     provider,
97     ///     LocaleFallbacker::new().static_to_owned(),
98     /// );
99     ///
100     /// // Now we can load the "de-CH" data via fallback to "de".
101     /// let german_hello_world: DataResponse<HelloWorldV1> = provider
102     ///     .load(DataRequest {
103     ///         id: id.as_borrowed(),
104     ///         ..Default::default()
105     ///     })
106     ///     .expect("Loading should succeed");
107     ///
108     /// assert_eq!("Hallo Welt", german_hello_world.payload.get().message);
109     /// ```
new(provider: P, fallbacker: LocaleFallbacker) -> Self110     pub fn new(provider: P, fallbacker: LocaleFallbacker) -> Self {
111         Self {
112             inner: provider,
113             fallbacker,
114         }
115     }
116 
117     /// Returns a reference to the inner provider, bypassing fallback.
inner(&self) -> &P118     pub fn inner(&self) -> &P {
119         &self.inner
120     }
121 
122     /// Returns a mutable reference to the inner provider.
inner_mut(&mut self) -> &mut P123     pub fn inner_mut(&mut self) -> &mut P {
124         &mut self.inner
125     }
126 
127     /// Returns ownership of the inner provider to the caller.
into_inner(self) -> P128     pub fn into_inner(self) -> P {
129         self.inner
130     }
131 
132     /// Run the fallback algorithm with the data request using the inner data provider.
133     /// Internal function; external clients should use one of the trait impls below.
134     ///
135     /// Function arguments:
136     ///
137     /// - F1 should perform a data load for a single DataRequest and return the result of it
138     /// - F2 should map from the provider-specific response type to DataResponseMetadata
run_fallback<F1, F2, R>( &self, marker: DataMarkerInfo, 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,139     fn run_fallback<F1, F2, R>(
140         &self,
141         marker: DataMarkerInfo,
142         mut base_req: DataRequest,
143         mut f1: F1,
144         mut f2: F2,
145     ) -> Result<R, DataError>
146     where
147         F1: FnMut(DataRequest) -> Result<R, DataError>,
148         F2: FnMut(&mut R) -> &mut DataResponseMetadata,
149     {
150         if marker.is_singleton {
151             return f1(base_req);
152         }
153         let mut fallback_iterator = self
154             .fallbacker
155             .for_config(marker.fallback_config)
156             .fallback_for(*base_req.id.locale);
157         let base_silent = core::mem::replace(&mut base_req.metadata.silent, true);
158         loop {
159             let result = f1(DataRequest {
160                 id: DataIdentifierBorrowed::for_marker_attributes_and_locale(
161                     base_req.id.marker_attributes,
162                     fallback_iterator.get(),
163                 ),
164                 ..base_req
165             });
166 
167             match result.allow_identifier_not_found() {
168                 Ok(Some(mut result)) => {
169                     f2(&mut result).locale = Some(fallback_iterator.take());
170                     return Ok(result);
171                 }
172                 Ok(None) => {
173                     // If we just checked und, break out of the loop.
174                     if fallback_iterator.get().is_default() {
175                         break;
176                     }
177                     fallback_iterator.step();
178                 }
179                 Err(e) => {
180                     // Log the original request rather than the fallback request
181                     base_req.metadata.silent = base_silent;
182                     return Err(e.with_req(marker, base_req));
183                 }
184             };
185         }
186         base_req.metadata.silent = base_silent;
187         Err(DataErrorKind::IdentifierNotFound.with_req(marker, base_req))
188     }
189 }
190 
191 impl<P, M> DynamicDataProvider<M> for LocaleFallbackProvider<P>
192 where
193     P: DynamicDataProvider<M>,
194     M: DynamicDataMarker,
195 {
load_data( &self, marker: DataMarkerInfo, req: DataRequest, ) -> Result<DataResponse<M>, DataError>196     fn load_data(
197         &self,
198         marker: DataMarkerInfo,
199         req: DataRequest,
200     ) -> Result<DataResponse<M>, DataError> {
201         self.run_fallback(
202             marker,
203             req,
204             |req| self.inner.load_data(marker, req),
205             |res| &mut res.metadata,
206         )
207     }
208 }
209 
210 impl<P, M> DynamicDryDataProvider<M> for LocaleFallbackProvider<P>
211 where
212     P: DynamicDryDataProvider<M>,
213     M: DynamicDataMarker,
214 {
dry_load_data( &self, marker: DataMarkerInfo, req: DataRequest, ) -> Result<DataResponseMetadata, DataError>215     fn dry_load_data(
216         &self,
217         marker: DataMarkerInfo,
218         req: DataRequest,
219     ) -> Result<DataResponseMetadata, DataError> {
220         self.run_fallback(
221             marker,
222             req,
223             |req| self.inner.dry_load_data(marker, req),
224             |m| m,
225         )
226     }
227 }
228 
229 impl<P, M> DataProvider<M> for LocaleFallbackProvider<P>
230 where
231     P: DataProvider<M>,
232     M: DataMarker,
233 {
load(&self, req: DataRequest) -> Result<DataResponse<M>, DataError>234     fn load(&self, req: DataRequest) -> Result<DataResponse<M>, DataError> {
235         self.run_fallback(
236             M::INFO,
237             req,
238             |req| self.inner.load(req),
239             |res| &mut res.metadata,
240         )
241     }
242 }
243 
244 impl<P, M> DryDataProvider<M> for LocaleFallbackProvider<P>
245 where
246     P: DryDataProvider<M>,
247     M: DataMarker,
248 {
dry_load(&self, req: DataRequest) -> Result<DataResponseMetadata, DataError>249     fn dry_load(&self, req: DataRequest) -> Result<DataResponseMetadata, DataError> {
250         self.run_fallback(M::INFO, req, |req| self.inner.dry_load(req), |m| m)
251     }
252 }
253 
254 #[test]
dry_test()255 fn dry_test() {
256     use icu_provider::hello_world::*;
257 
258     struct TestProvider;
259     impl DataProvider<HelloWorldV1> for TestProvider {
260         fn load(&self, _: DataRequest) -> Result<DataResponse<HelloWorldV1>, DataError> {
261             panic!("pretend this is super expensive")
262         }
263     }
264 
265     impl DryDataProvider<HelloWorldV1> for TestProvider {
266         fn dry_load(&self, req: DataRequest) -> Result<DataResponseMetadata, DataError> {
267             // We support all languages except English, and no regional variants. This is cheap to check.
268             if req.id.locale.region.is_some() || req.id.locale.language.as_str() == "en" {
269                 Err(DataErrorKind::IdentifierNotFound.into_error())
270             } else {
271                 Ok(Default::default())
272             }
273         }
274     }
275 
276     let provider =
277         LocaleFallbackProvider::new(TestProvider, LocaleFallbacker::new().static_to_owned());
278 
279     assert_eq!(
280         provider
281             .dry_load(DataRequest {
282                 id: DataIdentifierBorrowed::for_locale(&"de-CH".parse().unwrap()),
283                 ..Default::default()
284             })
285             .unwrap()
286             .locale,
287         "de".parse::<DataLocale>().ok()
288     );
289 
290     assert_eq!(
291         provider
292             .dry_load(DataRequest {
293                 id: DataIdentifierBorrowed::for_locale(&"en-GB".parse().unwrap()),
294                 ..Default::default()
295             })
296             .unwrap()
297             .locale,
298         Some(DataLocale::default())
299     );
300 }
301