message_locator/
lib.rs

1use std::{cell::{Cell, RefCell}, collections::{HashMap, HashSet}, sync::Arc};
2use maplit::{hashmap, hashset};
3use language_objects::{Language};
4use lazy_static::lazy_static;
5use lazy_regex::regex;
6
7/// Creates a `HashMap<String, String>` from a list of key-value pairs.
8///
9/// ## Example
10///
11/// ```
12/// use message_locator::locator_vars;
13/// fn main() {
14///     let map = locator_vars!{
15///         "a" => "foo",
16///         "b" => "bar",
17///     };
18///     assert_eq!(map[&"a".to_string()], "foo");
19///     assert_eq!(map[&"b".to_string()], "bar");
20///     assert_eq!(map.get(&"c".to_string()), None);
21/// }
22/// ```
23#[macro_export]
24macro_rules! locator_vars {
25    (@single $($x:tt)*) => (());
26    (@count $($rest:expr),*) => (<[()]>::len(&[$(locator_vars!(@single $rest)),*]));
27
28    ($($key:expr => $value:expr,)+) => { locator_vars!($($key => $value),+) };
29    ($($key:expr => $value:expr),*) => {
30        {
31            let _cap = locator_vars!(@count $($key),*);
32            let mut _map = ::std::collections::HashMap::<String, String>::with_capacity(_cap);
33            $(
34                let _ = _map.insert($key.to_string(), $value.to_string());
35            )*
36            _map
37        }
38    };
39}
40
41pub struct MessageLocator {
42    _current_locale: Option<Language>,
43    _locale_path_components: Arc<HashMap<Language, String>>,
44    _supported_locales: Arc<HashSet<Language>>,
45    _default_locale: Language,
46    _fallbacks: Arc<HashMap<Language, Vec<Language>>>,
47    _assets: Arc<HashMap<Language, serde_json::Value>>,
48    _assets_src: String,
49    _assets_base_file_names: Vec<String>,
50    _assets_clean_unused: bool,
51    _assets_load_method: MessageLocatorLoadMethod,
52}
53
54impl MessageLocator {
55    /// Constructs a `MessageLocator` object.
56    pub fn new(options: &MessageLocatorOptions) -> Self {
57        let mut locale_path_components = HashMap::<Language, String>::new();
58        let mut supported_locales = HashSet::<Language>::new();
59        for code in options._supported_locales.borrow().iter() {
60            let locale_parse = Language::parse(code).unwrap();
61            locale_path_components.insert(locale_parse.clone(), code.clone());
62            supported_locales.insert(locale_parse);
63        }
64        let mut fallbacks = HashMap::<Language, Vec<Language>>::new();
65        for (k, v) in options._fallbacks.borrow().iter() {
66            fallbacks.insert(Language::parse(k).unwrap(), v.iter().map(|s| Language::parse(s).unwrap()).collect());
67        }
68        let default_locale = options._default_locale.borrow().clone();
69        Self {
70            _current_locale: None,
71            _locale_path_components: Arc::new(locale_path_components),
72            _supported_locales: Arc::new(supported_locales),
73            _default_locale: Language::parse(&default_locale).unwrap(),
74            _fallbacks: Arc::new(fallbacks),
75            _assets: Arc::new(HashMap::new()),
76            _assets_src: options._assets.borrow()._src.borrow().clone(),
77            _assets_base_file_names: options._assets.borrow()._base_file_names.borrow().iter().map(|s| s.clone()).collect(),
78            _assets_clean_unused: options._assets.borrow()._clean_unused.get(),
79            _assets_load_method: options._assets.borrow()._load_method.get(),
80        }
81    }
82
83    /// Returns a set of supported locale codes, reflecting
84    /// the ones that were specified when constructing the `MessageLocator`.
85    pub fn supported_locales(&self) -> HashSet<Language> {
86        self._supported_locales.as_ref().clone()
87    }
88
89    /// Returns `true` if the locale is one of the supported locales
90    /// that were specified when constructing the `MessageLocator`,
91    /// otherwise `false`.
92    pub fn supports_locale(&self, arg: &Language) -> bool {
93        self._supported_locales.contains(arg)
94    }
95
96    /// Returns the currently loaded locale.
97    pub fn current_locale(&self) -> Option<Language> {
98        self._current_locale.clone()
99    }
100
101    /// Returns the currently loaded locale followed by its fallbacks or empty if no locale is loaded.
102    pub fn current_locale_seq(&self) -> HashSet<Language> {
103        if let Some(c) = self.current_locale() {
104            let mut r: HashSet<Language> = hashset![c.clone()];
105            self.enumerate_fallbacks(c.clone(), &mut r);
106            return r;
107        }
108        hashset![]
109    }
110
111    /// Attempts to load the specified locale and its fallbacks.
112    /// If any resource fails to load, the method returns `false`, otherwise `true`.
113    pub async fn update_locale(&mut self, new_locale: Language) -> bool {
114        self.load(Some(new_locale)).await
115    }
116
117    /// Attempts to load a locale and its fallbacks.
118    /// If the locale argument is specified, it is loaded.
119    /// Otherwise, if there is a default locale, it is loaded, and if not,
120    /// the method panics.
121    ///
122    /// If any resource fails to load, the method returns `false`, otherwise `true`.
123    pub async fn load(&mut self, mut new_locale: Option<Language>) -> bool {
124        if new_locale.is_none() { new_locale = Some(self._default_locale.clone()); }
125        let new_locale = new_locale.unwrap();
126        if !self.supports_locale(&new_locale) {
127            panic!("Unsupported locale {}", new_locale.tag());
128        }
129        let mut to_load: HashSet<Language> = hashset![new_locale.clone()];
130        self.enumerate_fallbacks(new_locale.clone(), &mut to_load);
131
132        let mut new_assets: HashMap<Language, serde_json::Value> = hashmap![];
133        for locale in to_load {
134            let res = self.load_single_locale(&locale).await;
135            if res.is_none() {
136                return false;
137            }
138            new_assets.insert(locale.clone(), res.unwrap());
139        }
140        if self._assets_clean_unused {
141            Arc::get_mut(&mut self._assets).unwrap().clear();
142        }
143
144        for (locale, root) in new_assets {
145            Arc::get_mut(&mut self._assets).unwrap().insert(locale, root);
146        }
147        self._current_locale = Some(new_locale.clone());
148        // let new_locale_code = unic_langid::LanguageIdentifier::from_bytes(new_locale.clone().standard_tag().to_string().as_ref()).unwrap();
149
150        true
151    }
152
153    async fn load_single_locale(&self, locale: &Language) -> Option<serde_json::Value> {
154        let mut r = serde_json::Value::Object(serde_json::Map::new());
155        match self._assets_load_method {
156            MessageLocatorLoadMethod::FileSystem => {
157                for base_name in self._assets_base_file_names.iter() {
158                    let locale_path_comp = self._locale_path_components.get(locale);
159                    if locale_path_comp.is_none() {
160                        panic!("Fallback locale is not supported a locale: {}", locale.tag());
161                    }
162                    let res_path = format!("{}/{}/{}.json", self._assets_src, locale_path_comp.unwrap(), base_name);
163                    let content = std::fs::read(res_path.clone());
164                    if content.is_err() {
165                        println!("Failed to load resource at {}.", res_path);
166                        return None;
167                    }
168                    MessageLocator::apply_deep(base_name, serde_json::from_str(String::from_utf8(content.unwrap()).unwrap().as_ref()).unwrap(), &mut r);
169                }
170            },
171            MessageLocatorLoadMethod::Http => {
172                for base_name in self._assets_base_file_names.iter() {
173                    let locale_path_comp = self._locale_path_components.get(locale);
174                    if locale_path_comp.is_none() {
175                        panic!("Fallback locale is not supported a locale: {}", locale.tag());
176                    }
177                    let res_path = format!("{}/{}/{}.json", self._assets_src, locale_path_comp.unwrap(), base_name);
178                    let content = reqwest::get(reqwest::Url::parse(res_path.clone().as_ref()).unwrap()).await;
179                    if content.is_err() {
180                        println!("Failed to load resource at {}.", res_path);
181                        return None;
182                    }
183                    let content = if content.is_ok() { Some(content.unwrap().text().await) } else { None };
184                    MessageLocator::apply_deep(base_name, serde_json::from_str(content.unwrap().unwrap().as_ref()).unwrap(), &mut r);
185                }
186            },
187        }
188        Some(r)
189    }
190
191    fn apply_deep(name: &String, assign: serde_json::Value, mut output: &mut serde_json::Value) {
192        let mut names: Vec<&str> = name.split("/").collect();
193        let last_name = names.pop();
194        for name in names {
195            let r = output.get(name);
196            if r.is_none() || r.unwrap().as_object().is_none() {
197                let r = serde_json::Value::Object(serde_json::Map::new());
198                output.as_object_mut().unwrap().insert(String::from(name), r);
199            }
200            output = output.get_mut(name).unwrap();
201        }
202        output.as_object_mut().unwrap().insert(String::from(last_name.unwrap()), assign);
203    }
204
205    fn enumerate_fallbacks(&self, locale: Language, output: &mut HashSet<Language>) {
206        for list in self._fallbacks.get(&locale).iter() {
207            for item in list.iter() {
208                output.insert(item.clone());
209                self.enumerate_fallbacks(item.clone(), output);
210            }
211        }
212    }
213
214    /// Retrieves message by identifier.
215    pub fn get<S: ToString>(&self, id: S) -> String {
216        self.get_formatted(id, vec![])
217    }
218
219    /// Retrieves message by identifier with formatting arguments.
220    pub fn get_formatted<S: ToString>(&self, id: S, options: Vec<&dyn MessageLocatorFormatArgument>) -> String {
221        let mut variables: Option<HashMap<String, String>> = None;
222        let mut id = id.to_string();
223
224        for option in options.iter() {
225            if let Some(r) = option.as_str() {
226                id.push('_');
227                id.push_str(r);
228            }
229            else if let Some(r) = option.as_string() {
230                id.push('_');
231                id.push_str(r.as_str());
232            }
233            else if let Some(r) = option.as_string_map() {
234                variables = Some(r.iter().map(|(k, v)| (k.clone(), v.clone())).collect());
235            }
236        }
237
238        if variables.is_none() { variables = Some(HashMap::new()); }
239        let variables = variables.unwrap();
240
241        let id: Vec<String> = id.split(".").map(|s| s.to_string()).collect();
242        if self._current_locale.is_none() {
243            return id.join(".");
244        }
245        let r = self.get_formatted_with_locale(self._current_locale.clone().unwrap(), &id, &variables);
246        if let Some(r) = r { r } else { id.join(".") }
247    }
248
249    fn get_formatted_with_locale(&self, locale: Language, id: &Vec<String>, vars: &HashMap<String, String>) -> Option<String> {
250        let message = self.resolve_id(self._assets.get(&locale), id);
251        if message.is_some() {
252            return Some(self.apply_message(message.unwrap(), vars));
253        }
254
255        let fallbacks = self._fallbacks.get(&locale);
256        if fallbacks.is_some() {
257            for fl in fallbacks.unwrap().iter() {
258                let r = self.get_formatted_with_locale(fl.clone(), id, vars);
259                if r.is_some() {
260                    return r;
261                }
262            }
263        }
264        None
265    }
266
267    fn apply_message(&self, message: String, vars: &HashMap<String, String>) -> String {
268        // regex!(r"\$(\$|[A-Za-z0-9_-]+)").replace_all(&message, R { _vars: vars }).as_ref().to_string()
269        regex!(r"\$(\$|[A-Za-z0-9_-]+)").replace_all(&message, |s: &regex::Captures<'_>| {
270            let s = s.get(0).unwrap().as_str();
271            if s == "$$" {
272                "$"
273            } else {
274                let v = vars.get(&s.to_string().replace("$", ""));
275                if let Some(v) = v { v } else { "undefined" }
276            }
277        }).as_ref().to_string()
278    }
279
280    fn resolve_id(&self, root: Option<&serde_json::Value>, id: &Vec<String>) -> Option<String> {
281        let mut r = root;
282        for frag in id.iter() {
283            if r.is_none() {
284                return None;
285            }
286            r = r.unwrap().get(frag);
287        }
288        if r.is_none() {
289            return None;
290        }
291        let r = r.unwrap().as_str();
292        if let Some(r) = r { Some(r.to_string()) } else { None }
293    }
294}
295
296impl Clone for MessageLocator {
297    /// Clones the locator, sharing the same
298    /// resources.
299    fn clone(&self) -> Self {
300        Self {
301            _current_locale: self._current_locale.clone(),
302            _locale_path_components: self._locale_path_components.clone(),
303            _supported_locales: self._supported_locales.clone(),
304            _default_locale: self._default_locale.clone(),
305            _fallbacks: self._fallbacks.clone(),
306            _assets: self._assets.clone(),
307            _assets_src: self._assets_src.clone(),
308            _assets_base_file_names: self._assets_base_file_names.clone(),
309            _assets_clean_unused: self._assets_clean_unused,
310            _assets_load_method: self._assets_load_method,
311        }
312    }
313}
314
315pub trait MessageLocatorFormatArgument {
316    fn as_str(&self) -> Option<&'static str> { None }
317    fn as_string(&self) -> Option<String> { None }
318    fn as_string_map(&self) -> Option<HashMap<String, String>> { None }
319}
320
321impl MessageLocatorFormatArgument for &'static str {
322    fn as_str(&self) -> Option<&'static str> { Some(self) }
323}
324
325impl MessageLocatorFormatArgument for String {
326    fn as_string(&self) -> Option<String> { Some(self.clone()) }
327}
328
329impl MessageLocatorFormatArgument for HashMap<String, String> {
330    fn as_string_map(&self) -> Option<HashMap<String, String>> { Some(self.clone()) }
331}
332
333impl MessageLocatorFormatArgument for i8 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
334impl MessageLocatorFormatArgument for i16 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
335impl MessageLocatorFormatArgument for i32 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
336impl MessageLocatorFormatArgument for i64 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
337impl MessageLocatorFormatArgument for i128 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
338impl MessageLocatorFormatArgument for isize { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
339impl MessageLocatorFormatArgument for u8 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
340impl MessageLocatorFormatArgument for u16 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
341impl MessageLocatorFormatArgument for u32 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
342impl MessageLocatorFormatArgument for u64 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
343impl MessageLocatorFormatArgument for u128 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
344impl MessageLocatorFormatArgument for usize { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
345impl MessageLocatorFormatArgument for f32 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
346impl MessageLocatorFormatArgument for f64 { fn as_string(&self) -> Option<String> { Some(self.to_string()) } }
347
348pub struct MessageLocatorOptions {
349    _default_locale: RefCell<String>,
350    _supported_locales: RefCell<Vec<String>>,
351    _fallbacks: RefCell<HashMap<String, Vec<String>>>,
352    _assets: RefCell<MessageLocatorAssetOptions>,
353}
354
355impl MessageLocatorOptions {
356    pub fn new() -> Self {
357        MessageLocatorOptions {
358            _default_locale: RefCell::new("en".to_string()),
359            _supported_locales: RefCell::new(vec!["en".to_string()]),
360            _fallbacks: RefCell::new(hashmap! {}),
361            _assets: RefCell::new(MessageLocatorAssetOptions::new()),
362        }
363    }
364
365    pub fn default_locale<S: ToString>(&self, value: S) -> &Self {
366        self._default_locale.replace(value.to_string());
367        self
368    }
369
370    pub fn supported_locales<S: ToString>(&self, list: Vec<S>) -> &Self {
371        self._supported_locales.replace(list.iter().map(|name| name.to_string()).collect());
372        self
373    }
374
375    pub fn fallbacks<S: ToString>(&self, map: HashMap<S, Vec<S>>) -> &Self {
376        self._fallbacks.replace(map.iter().map(|(k, v)| (
377            k.to_string(),
378            v.iter().map(|s| s.to_string()).collect()
379        )).collect());
380        self
381    }
382
383    pub fn assets(&self, options: &MessageLocatorAssetOptions) -> &Self {
384        self._assets.replace(options.clone());
385        self
386    }
387}
388
389pub struct MessageLocatorAssetOptions {
390    _src: RefCell<String>,
391    _base_file_names: RefCell<Vec<String>>,
392    _clean_unused: Cell<bool>,
393    _load_method: Cell<MessageLocatorLoadMethod>,
394}
395
396impl Clone for MessageLocatorAssetOptions {
397    fn clone(&self) -> Self {
398        Self {
399            _src: self._src.clone(),
400            _base_file_names: self._base_file_names.clone(),
401            _clean_unused: self._clean_unused.clone(),
402            _load_method: self._load_method.clone(),
403        }
404    }
405}
406
407impl MessageLocatorAssetOptions {
408    pub fn new() -> Self {
409        MessageLocatorAssetOptions {
410            _src: RefCell::new("res/lang".to_string()),
411            _base_file_names: RefCell::new(vec![]),
412            _clean_unused: Cell::new(true),
413            _load_method: Cell::new(MessageLocatorLoadMethod::Http),
414        }
415    }
416    
417    pub fn src<S: ToString>(&self, src: S) -> &Self {
418        self._src.replace(src.to_string());
419        self
420    } 
421
422    pub fn base_file_names<S: ToString>(&self, list: Vec<S>) -> &Self {
423        self._base_file_names.replace(list.iter().map(|name| name.to_string()).collect());
424        self
425    }
426
427    pub fn clean_unused(&self, value: bool) -> &Self {
428        self._clean_unused.set(value);
429        self
430    }
431
432    pub fn load_method(&self, value: MessageLocatorLoadMethod) -> &Self {
433        self._load_method.set(value);
434        self
435    }
436}
437
438#[derive(Copy, Clone)]
439pub enum MessageLocatorLoadMethod {
440    FileSystem,
441    Http,
442}