Skip to content

Commit 6e1aaa4

Browse files
authored
Add test coverage for fluent-resmgr (#294)
1 parent 1c2e02b commit 6e1aaa4

File tree

4 files changed

+145
-9
lines changed

4 files changed

+145
-9
lines changed

fluent-resmgr/examples/simple-resmgr.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ fn main() {
7979
.collect()
8080
});
8181

82-
// 3. Negotiate it against the avialable ones
82+
// 3. Negotiate it against the available ones
8383
let default_locale: LanguageIdentifier = "en-US".parse().expect("Parsing failed.");
8484
let available = get_available_locales().expect("Retrieving available locales failed.");
8585
let resolved_locales = negotiate_languages(

fluent-resmgr/src/resource_manager.rs

Lines changed: 111 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,44 @@ fn read_file(path: &str) -> Result<String, io::Error> {
1515
fs::read_to_string(path)
1616
}
1717

18+
/// [ResourceManager] provides a standalone solution for managing localization resources which
19+
/// can be used by `fluent-fallback` or other higher level bindings.
1820
pub struct ResourceManager {
1921
resources: FrozenMap<String, Box<FluentResource>>,
2022
path_scheme: String,
2123
}
2224

2325
impl ResourceManager {
26+
/// Create a new and empty [`ResourceManager`]. As resources are added they will be
27+
/// retained in the `resources` [`FrozenMap`]. The `path_scheme` argument defines
28+
/// how the files are organized.
29+
///
30+
/// For instance `"./translations/{locale}/{res_id}"` will load files with the
31+
/// following structure:
32+
///
33+
/// .
34+
/// └── translations
35+
///     ├── en-US
36+
///     │   ├── app.ftl
37+
///     │   └── errors.ftl
38+
///     └── pl
39+
///     ├── app.ftl
40+
///     └── errors.ftl
41+
///
2442
pub fn new(path_scheme: String) -> Self {
2543
ResourceManager {
2644
resources: FrozenMap::new(),
2745
path_scheme,
2846
}
2947
}
3048

31-
fn get_resource(&self, res_id: &str, locale: &str) -> &FluentResource {
49+
/// Returns a [`FluentResource`], by either reading the file and loading it into
50+
/// memory, or retrieving it from an in-memory cache.
51+
fn get_resource(&self, resource_id: &str, locale: &str) -> &FluentResource {
3252
let path = self
3353
.path_scheme
3454
.replace("{locale}", locale)
35-
.replace("{res_id}", res_id);
55+
.replace("{res_id}", resource_id);
3656
if let Some(res) = self.resources.get(&path) {
3757
res
3858
} else {
@@ -45,34 +65,44 @@ impl ResourceManager {
4565
}
4666
}
4767

68+
/// Gets a [`FluentBundle`] from a list of resources. The bundle will only contain the
69+
/// resources from the first locale in the locales list. The other locales will be
70+
/// stored in the [`FluentBundle`] and will only be used for custom formatters such
71+
/// date time format, or plural rules. The message formatting will not fall back
72+
/// to other locales.
4873
pub fn get_bundle(
4974
&self,
5075
locales: Vec<LanguageIdentifier>,
5176
resource_ids: Vec<String>,
5277
) -> FluentBundle<&FluentResource> {
5378
let mut bundle = FluentBundle::new(locales.clone());
5479
for res_id in &resource_ids {
80+
println!("res_id {:?}", res_id);
5581
let res = self.get_resource(res_id, &locales[0].to_string());
5682
bundle.add_resource(res).unwrap();
5783
}
5884
bundle
5985
}
6086

87+
/// Returns an iterator for a [`FluentBundle`] for each locale provided. Each
88+
/// iteration will load all of the resources for that single locale. i18n formatters
89+
/// such as date time format and plural rules will ignore the list of locales,
90+
/// unlike `get_bundle` and only use the single locale of the bundle.
6191
pub fn get_bundles(
6292
&self,
6393
locales: Vec<LanguageIdentifier>,
6494
resource_ids: Vec<String>,
6595
) -> impl Iterator<Item = FluentBundle<&FluentResource>> {
6696
let res_mgr = self;
67-
let mut ptr = 0;
97+
let mut idx = 0;
6898

6999
iter::from_fn(move || {
70-
locales.get(ptr).map(|locale| {
71-
ptr += 1;
100+
locales.get(idx).map(|locale| {
101+
idx += 1;
72102
let mut bundle = FluentBundle::new(vec![locale.clone()]);
73-
for res_id in &resource_ids {
74-
let res = res_mgr.get_resource(res_id, &locale.to_string());
75-
bundle.add_resource(res).unwrap();
103+
for resource_id in &resource_ids {
104+
let resource = res_mgr.get_resource(resource_id, &locale.to_string());
105+
bundle.add_resource(resource).unwrap();
76106
}
77107
bundle
78108
})
@@ -105,6 +135,10 @@ impl Iterator for BundleIter {
105135
}
106136
}
107137

138+
// TODO - These need to be implemented.
139+
// https://github.com/projectfluent/fluent-rs/issues/281
140+
141+
// coverage(off)
108142
impl Stream for BundleIter {
109143
type Item = FluentBundleResult<FluentResource>;
110144

@@ -138,3 +172,72 @@ impl BundleGenerator for ResourceManager {
138172
todo!()
139173
}
140174
}
175+
// coverage(on)
176+
177+
#[cfg(test)]
178+
mod test {
179+
use super::*;
180+
use unic_langid::langid;
181+
182+
#[test]
183+
fn caching() {
184+
let res_mgr = ResourceManager::new("./tests/resources/{locale}/{res_id}".into());
185+
186+
let _bundle = res_mgr.get_bundle(vec![langid!("en-US")], vec!["test.ftl".into()]);
187+
let res_1 = res_mgr.get_resource("test.ftl", "en-US");
188+
189+
let _bundle2 = res_mgr.get_bundle(vec![langid!("en-US")], vec!["test.ftl".into()]);
190+
let res_2 = res_mgr.get_resource("test.ftl", "en-US");
191+
192+
assert!(
193+
std::ptr::eq(res_1, res_2),
194+
"The resources are cached in memory and reference the same thing."
195+
);
196+
}
197+
198+
// TODO - This API should return a Result instead.
199+
// https://github.com/projectfluent/fluent-rs/issues/278
200+
#[test]
201+
#[should_panic]
202+
fn get_resource_error() {
203+
let res_mgr = ResourceManager::new("./tests/resources/{locale}/{res_id}".into());
204+
205+
let _bundle = res_mgr.get_bundle(vec![langid!("en-US")], vec!["test.ftl".into()]);
206+
res_mgr.get_resource("nonexistent.ftl", "en-US");
207+
}
208+
209+
// TODO - This API should return a Result instead.
210+
// https://github.com/projectfluent/fluent-rs/issues/278
211+
#[test]
212+
#[should_panic]
213+
fn get_bundle_error() {
214+
let res_mgr = ResourceManager::new("./tests/resources/{locale}/{res_id}".into());
215+
let _bundle = res_mgr.get_bundle(vec![langid!("en-US")], vec!["nonexistent.ftl".into()]);
216+
}
217+
218+
// TODO - Syntax errors should be surfaced. This test has an invalid resource that
219+
// should fail, but currently isn't.
220+
// https://github.com/projectfluent/fluent-rs/issues/280
221+
#[test]
222+
fn get_bundle_ignores_errors() {
223+
let res_mgr = ResourceManager::new("./tests/resources/{locale}/{res_id}".into());
224+
let bundle = res_mgr.get_bundle(
225+
vec![langid!("en-US")],
226+
vec!["test.ftl".into(), "invalid.ftl".into()],
227+
);
228+
229+
let mut errors = vec![];
230+
let msg = bundle.get_message("hello-world").expect("Message exists");
231+
let pattern = msg.value().expect("Message has a value");
232+
let value = bundle.format_pattern(&pattern, None, &mut errors);
233+
assert_eq!(value, "Hello World");
234+
assert!(errors.is_empty());
235+
236+
let mut errors = vec![];
237+
let msg = bundle.get_message("valid-message").expect("Message exists");
238+
let pattern = msg.value().expect("Message has a value");
239+
let value = bundle.format_pattern(&pattern, None, &mut errors);
240+
assert_eq!(value, "This is a valid message");
241+
assert!(errors.is_empty());
242+
}
243+
}

fluent-resmgr/tests/localization_test.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,33 @@ fn resmgr_get_bundle() {
4444
let value = bundle.format_pattern(pattern, None, &mut errors);
4545
assert_eq!(value, "Hello World");
4646
}
47+
48+
#[test]
49+
fn resmgr_get_bundles() {
50+
let res_mgr = ResourceManager::new("./tests/resources/{locale}/{res_id}".into());
51+
52+
let locales = vec![langid!("en-US"), langid!("pl")];
53+
let mut bundles_iter = res_mgr.get_bundles(locales.clone(), vec!["test.ftl".into()]);
54+
55+
{
56+
let bundle = bundles_iter.next().expect("Failed to get en-US bundle.");
57+
58+
let mut errors = vec![];
59+
let msg = bundle.get_message("hello-world").expect("Message exists");
60+
let pattern = msg.value().expect("Message has a value");
61+
let value = bundle.format_pattern(&pattern, None, &mut errors);
62+
assert_eq!(value, "Hello World");
63+
}
64+
65+
{
66+
let bundle = bundles_iter.next().expect("Failed to get pl bundle.");
67+
68+
let mut errors = vec![];
69+
let msg = bundle.get_message("hello-world").expect("Witaj Świecie");
70+
let pattern = msg.value().expect("Message has a value");
71+
let value = bundle.format_pattern(&pattern, None, &mut errors);
72+
assert_eq!(value, "Witaj Świecie");
73+
}
74+
75+
assert!(bundles_iter.next().is_none(), "The iterator is consumed.");
76+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
valid-message = This is a valid message
2+
3+
However, this will be a syntax error.

0 commit comments

Comments
 (0)