fluent_resmgr/
resource_manager.rs1use elsa::FrozenMap;
2use fluent_bundle::{FluentBundle, FluentResource};
3use fluent_fallback::{
4 generator::{BundleGenerator, FluentBundleResult},
5 types::ResourceId,
6};
7use futures::stream::Stream;
8use rustc_hash::FxHashSet;
9use std::io;
10use std::{fs, iter};
11use thiserror::Error;
12use unic_langid::LanguageIdentifier;
13
14fn read_file(path: &str) -> Result<String, io::Error> {
15 fs::read_to_string(path)
16}
17
18pub struct ResourceManager {
21 resources: FrozenMap<String, Box<FluentResource>>,
22 path_scheme: String,
23}
24
25impl ResourceManager {
26 pub fn new(path_scheme: String) -> Self {
43 ResourceManager {
44 resources: FrozenMap::new(),
45 path_scheme,
46 }
47 }
48
49 fn get_resource(
52 &self,
53 resource_id: &str,
54 locale: &str,
55 ) -> Result<&FluentResource, ResourceManagerError> {
56 let path = self
57 .path_scheme
58 .replace("{locale}", locale)
59 .replace("{res_id}", resource_id);
60 Ok(if let Some(resource) = self.resources.get(&path) {
61 resource
62 } else {
63 let resource = match FluentResource::try_new(read_file(&path)?) {
64 Ok(resource) => resource,
65 Err((resource, _err)) => resource,
66 };
67 self.resources.insert(path.to_string(), Box::new(resource))
68 })
69 }
70
71 pub fn get_bundle(
77 &self,
78 locales: Vec<LanguageIdentifier>,
79 resource_ids: Vec<String>,
80 ) -> Result<FluentBundle<&FluentResource>, Vec<ResourceManagerError>> {
81 let mut errors: Vec<ResourceManagerError> = vec![];
82 let mut bundle = FluentBundle::new(locales.clone());
83 let locale = &locales[0];
84
85 for resource_id in &resource_ids {
86 match self.get_resource(resource_id, &locale.to_string()) {
87 Ok(resource) => {
88 if let Err(errs) = bundle.add_resource(resource) {
89 for error in errs {
90 errors.push(ResourceManagerError::Fluent(error));
91 }
92 }
93 }
94 Err(error) => errors.push(error),
95 };
96 }
97
98 if errors.is_empty() {
99 Ok(bundle)
100 } else {
101 Err(errors)
102 }
103 }
104
105 pub fn get_bundles(
110 &self,
111 locales: Vec<LanguageIdentifier>,
112 resource_ids: Vec<String>,
113 ) -> impl Iterator<Item = Result<FluentBundle<&FluentResource>, Vec<ResourceManagerError>>>
114 {
115 let mut idx = 0;
116
117 iter::from_fn(move || {
118 locales.get(idx).map(|locale| {
119 idx += 1;
120 let mut errors: Vec<ResourceManagerError> = vec![];
121 let mut bundle = FluentBundle::new(vec![locale.clone()]);
122
123 for resource_id in &resource_ids {
124 match self.get_resource(resource_id, &locale.to_string()) {
125 Ok(resource) => {
126 if let Err(errs) = bundle.add_resource(resource) {
127 for error in errs {
128 errors.push(ResourceManagerError::Fluent(error));
129 }
130 }
131 }
132 Err(error) => errors.push(error),
133 }
134 }
135
136 if !errors.is_empty() {
137 Err(errors)
138 } else {
139 Ok(bundle)
140 }
141 })
142 })
143 }
144}
145
146#[derive(Debug, Error)]
148pub enum ResourceManagerError {
149 #[error("{0}")]
151 Io(#[from] std::io::Error),
152
153 #[error("{0}")]
155 Fluent(#[from] fluent_bundle::FluentError),
156}
157
158pub struct BundleIter {
161 locales: <Vec<LanguageIdentifier> as IntoIterator>::IntoIter,
162 res_ids: FxHashSet<ResourceId>,
163}
164
165impl Iterator for BundleIter {
166 type Item = FluentBundleResult<FluentResource>;
167
168 fn next(&mut self) -> Option<Self::Item> {
169 let locale = self.locales.next()?;
170
171 let mut bundle = FluentBundle::new(vec![locale.clone()]);
172
173 for res_id in self.res_ids.iter() {
174 let full_path = format!("./tests/resources/{}/{}", locale, res_id);
175 let source = fs::read_to_string(full_path).unwrap();
176 let res = FluentResource::try_new(source).unwrap();
177 bundle.add_resource(res).unwrap();
178 }
179 Some(Ok(bundle))
180 }
181}
182
183impl Stream for BundleIter {
188 type Item = FluentBundleResult<FluentResource>;
189
190 fn poll_next(
191 self: std::pin::Pin<&mut Self>,
192 _cx: &mut std::task::Context<'_>,
193 ) -> std::task::Poll<Option<Self::Item>> {
194 todo!()
195 }
196}
197
198impl BundleGenerator for ResourceManager {
199 type Resource = FluentResource;
200 type LocalesIter = std::vec::IntoIter<LanguageIdentifier>;
201 type Iter = BundleIter;
202 type Stream = BundleIter;
203
204 fn bundles_iter(
205 &self,
206 locales: Self::LocalesIter,
207 res_ids: FxHashSet<ResourceId>,
208 ) -> Self::Iter {
209 BundleIter { locales, res_ids }
210 }
211
212 fn bundles_stream(
213 &self,
214 _locales: Self::LocalesIter,
215 _res_ids: FxHashSet<ResourceId>,
216 ) -> Self::Stream {
217 todo!()
218 }
219}
220#[cfg(test)]
223mod test {
224 use super::*;
225 use unic_langid::langid;
226
227 #[test]
228 fn caching() {
229 let res_mgr = ResourceManager::new("./tests/resources/{locale}/{res_id}".into());
230
231 let _bundle = res_mgr.get_bundle(vec![langid!("en-US")], vec!["test.ftl".into()]);
232 let res_1 = res_mgr
233 .get_resource("test.ftl", "en-US")
234 .expect("Could not get resource");
235
236 let _bundle2 = res_mgr.get_bundle(vec![langid!("en-US")], vec!["test.ftl".into()]);
237 let res_2 = res_mgr
238 .get_resource("test.ftl", "en-US")
239 .expect("Could not get resource");
240
241 assert!(
242 std::ptr::eq(res_1, res_2),
243 "The resources are cached in memory and reference the same thing."
244 );
245 }
246
247 #[test]
248 fn get_resource_error() {
249 let res_mgr = ResourceManager::new("./tests/resources/{locale}/{res_id}".into());
250
251 let _bundle = res_mgr.get_bundle(vec![langid!("en-US")], vec!["test.ftl".into()]);
252 let res = res_mgr.get_resource("nonexistent.ftl", "en-US");
253
254 assert!(res.is_err());
255 }
256
257 #[test]
258 fn get_bundle_error() {
259 let res_mgr = ResourceManager::new("./tests/resources/{locale}/{res_id}".into());
260 let bundle = res_mgr.get_bundle(vec![langid!("en-US")], vec!["nonexistent.ftl".into()]);
261
262 assert!(bundle.is_err());
263 }
264
265 #[test]
269 fn get_bundle_ignores_errors() {
270 let res_mgr = ResourceManager::new("./tests/resources/{locale}/{res_id}".into());
271 let bundle = res_mgr
272 .get_bundle(
273 vec![langid!("en-US")],
274 vec!["test.ftl".into(), "invalid.ftl".into()],
275 )
276 .expect("Could not retrieve bundle");
277
278 let mut errors = vec![];
279 let msg = bundle.get_message("hello-world").expect("Message exists");
280 let pattern = msg.value().expect("Message has a value");
281 let value = bundle.format_pattern(pattern, None, &mut errors);
282 assert_eq!(value, "Hello World");
283 assert!(errors.is_empty());
284
285 let mut errors = vec![];
286 let msg = bundle.get_message("valid-message").expect("Message exists");
287 let pattern = msg.value().expect("Message has a value");
288 let value = bundle.format_pattern(pattern, None, &mut errors);
289 assert_eq!(value, "This is a valid message");
290 assert!(errors.is_empty());
291 }
292}