sorrow_i18n/
lib.rs

1//! Implements a simpler version of I18n.
2//! Supports 2 built-in data providers (for static projects where the files do not change and where the localization file can be changed by the user or developer).
3//! For other cases, you can write your own data provider.
4//! [Github project](https://github.com/SinmoWay/simple-i18n)
5//! [Crates](https://crates.io/crates/sorrow-i18n)
6
7#![deny(missing_docs)]
8#![deny(warnings)]
9
10/// Macro feature.
11/// Adds 2 macros, the first one serves for initialization, the second one for getting the value from the holders.
12///
13/// # Examples
14///
15/// ```
16/// // Let's initialize our i18n core.
17/// init_i18n!("locale/");
18///
19/// // Getting data by holder. (locale is required)
20/// // If the key is not found or the locale is not found, return the passed key.
21/// let test = i18n!("RU", "data.name");
22/// assert_eq!("Тест", &*test);
23/// let not_found_data = i18n!("RU", "data.not_found_me");
24/// assert_eq!("data.not_found_me", &*not_found_data);
25/// ```
26#[cfg(feature = "macro")]
27pub mod feature_macro;
28
29use std::collections::HashMap;
30use std::fs::{File};
31use std::io::Read;
32use std::path::Path;
33use std::sync::{Arc, Mutex, RwLock};
34use std::thread::sleep;
35use std::time::Duration;
36use sys_locale::get_locale;
37
38use err_derive::Error;
39#[cfg(feature = "incl_dir")]
40use include_dir::Dir;
41use notify::{ErrorKind, RecommendedWatcher, RecursiveMode, Watcher};
42use serde_yaml::Value;
43
44/// Error type
45pub type Error = I18nError;
46
47/// Library errors
48#[derive(Debug, Error)]
49pub enum I18nError {
50    /// Access denied for file.
51    /// Not found file and e.t.c.
52    #[error(display = "File with path {:?} not found.", path)]
53    IoError {
54        /// The file that generated the error
55        path: String,
56        /// Cause message
57        cause: String,
58    },
59
60    /// Invalid structure locale file.
61    #[error(display = "Structure with path {:?} invalid. Additional information: {:?}", path, cause)]
62    InvalidStructure {
63        /// The file that generated the error
64        path: String,
65        /// Cause message
66        cause: String,
67    },
68
69    /// Invalid kind of file.
70    #[error(display = "Structure with path {:?} invalid. Expected kind: I18N.", path)]
71    InvalidHeader {
72        /// The file that generated the error
73        path: String,
74    },
75
76    /// Error while watching by file.
77    #[error(display = "Watching by file return error: {:?}", message)]
78    WatchError {
79        /// Cause message
80        message: String
81    },
82
83    /// File extension is not .yaml or .yml
84    #[error(display = "File type is not supported: {:?}", path)]
85    NotSupportedFileExtension {
86        /// The file that generated the error
87        path: String
88    },
89
90    /// The error is generated when you have two files with the same locale or when you manually add an existing locale.
91    #[error(display = "Duplicate locale holder for {:?}", locale)]
92    DuplicateLocale {
93        /// Duplicate locale
94        locale: String
95    },
96
97    /// An error due to which the provider was not added to the locale.
98    #[error(display = "The provider has not been added to the {:?} locale. Cause: {:?}", locale, cause)]
99    ProviderNotAddedError {
100        /// Locale where provider return error
101        locale: String,
102        /// Cause error
103        cause: String,
104    },
105}
106
107/// Implementation of the state observer.
108/// You can implement your own observer and add it to the MessageHolder, for this see an example in examples/custom_provider.rs
109///
110/// # Implementation
111///
112/// The library provides 2 types of observers:
113/// 1) For static files (does not imply changing them) [StaticFileProvider]
114/// 2) For files that you plan to modify in any way. Or do you mean such a possibility. [FileProvider]
115///
116/// # Examples for custom provider
117/// Creating base structure for provider.
118/// The provider must work with information, which means he must receive a link to the working data.
119///
120/// ```
121/// use std::collections::HashMap;
122/// use std::sync::{Arc, RwLock};
123///
124/// pub struct CustomProvider {
125///     data: Arc<RwLock<HashMap<String, String>>>,
126/// }
127/// ```
128/// ## Implementation WatchProvider for provider
129///
130///
131/// ```
132/// use std::collections::HashMap;
133/// use std::sync::{Arc, RwLock};
134/// use sorrow_i18n::{Error, WatchProvider};
135///
136/// impl WatchProvider for CustomProvider {
137///     fn watch(&mut self) -> Result<(), Error> {
138///         println!("Accepted custom provider");
139///         Ok(())
140///     }
141///
142/// // Setting current data in holder.
143///     fn set_data(&mut self, data: Arc<RwLock<HashMap<String, String>>>) -> Result<(), Error> {
144///         self.data = data;
145///         println!("Data has been set");
146///         Ok(())
147///     }
148/// }
149///
150/// ```
151///
152/// # Using in project
153///
154/// Add provider for holder.
155///
156/// ```
157///     use sorrow_i18n::InternationalCore;
158///
159///     let mut core = InternationalCore::new("resources/locales");
160///     core.add_provider("my locale", Box::new(CustomProvider::new())).unwrap();
161/// ```
162///
163pub trait WatchProvider {
164    /// The main observer method that is called to observe the state.
165    fn watch(&mut self) -> Result<(), Error>;
166
167    /// Setter for data reference.
168    fn set_data(&mut self, data: Arc<RwLock<HashMap<String, String>>>) -> Result<(), Error>;
169}
170
171/// Base providers
172#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
173pub enum Providers {
174    /// [FileProvider] - dynamically watcher for file.
175    FileProvider,
176    /// [StaticFileProvider] - static file. It is not being watched. Default option if the `provider` is not specified in the file structure
177    StaticFileProvider,
178}
179
180/// Files maybe changed. Watch by `modify` system event.
181struct FileProvider {
182    messages: Arc<RwLock<HashMap<String, String>>>,
183    path: String,
184    watcher: Option<RecommendedWatcher>,
185}
186
187impl FileProvider {
188    pub fn new(messages: Arc<RwLock<HashMap<String, String>>>, path: String) -> Self {
189        FileProvider {
190            messages,
191            path,
192            watcher: None,
193        }
194    }
195}
196
197impl WatchProvider for FileProvider {
198    fn watch(&mut self) -> Result<(), Error> {
199        let holder = Arc::clone(&self.messages);
200        let path = self.path.clone();
201        let res_watcher = notify::recommended_watcher(move |result: Result<notify::Event, notify::Error>| {
202            let event = result.map_err(|e| Error::WatchError { message: e.to_string() }).unwrap();
203            if event.kind.is_modify() {
204                // Hack.
205                // Inappropriate library behavior was detected when the file was updated on the Winodws platform.
206                // For some reason, 2 save events are fired, which causes double reads of the file.
207                // At the same time, the intervals between reading the file (updated configuration) are too small, which causes an error in the form of EOF.
208                // The simplest solution is to set a minimum timeout between these events.
209                sleep(Duration::from_millis(10));
210                log::debug!("Modify {}. Reloading data.", &path.clone());
211                // Validation file
212                let structure = load_struct(&path.clone()).unwrap();
213                // Lock data and clear
214                let mut w_holder = holder.write().unwrap();
215                w_holder.clear();
216
217                // Clone internal state.
218                let l_holder = structure.messages.write().unwrap().clone();
219                w_holder.extend(l_holder);
220                // Unlock
221            }
222        });
223
224        return match res_watcher {
225            Ok(mut w) => {
226                // TODO: Check error's?
227                w.watch(Path::new(&self.path.clone()), RecursiveMode::NonRecursive).unwrap();
228                self.watcher = Some(w);
229                Ok(())
230            }
231
232            Err(e) => {
233                match e.kind {
234                    ErrorKind::Generic(message) => {
235                        log::error!("Error while watch by file {}. Message: {}", &self.path, &message);
236                        Err(Error::WatchError { message })
237                    }
238                    ErrorKind::Io(err) => {
239                        log::error!("Error while watch by file {}. Message: {}", &self.path, &err);
240                        Err(Error::WatchError { message: err.to_string() })
241                    }
242                    ErrorKind::PathNotFound => {
243                        log::error!("Path not found: {}", &self.path);
244                        Err(Error::IoError { path: self.path.clone(), cause: String::default() })
245                    }
246                    ErrorKind::WatchNotFound => {
247                        log::error!("Watcher not found for: {}", &self.path);
248                        // Ignore
249                        Ok(())
250                    }
251                    ErrorKind::InvalidConfig(err) => {
252                        log::error!("Invalid watch config. {:?}", &err);
253                        // Ignore
254                        Ok(())
255                    }
256                    ErrorKind::MaxFilesWatch => {
257                        log::error!("Max watchers for: {}", &self.path);
258                        Err(Error::WatchError { message: String::from("max watchers for file. Please try again latter or remove exists watcher.") })
259                    }
260                }
261            }
262        };
263    }
264
265    fn set_data(&mut self, data: Arc<RwLock<HashMap<String, String>>>) -> Result<(), Error> {
266        self.messages = data;
267        Ok(())
268    }
269}
270
271/// Files does not changed. Only loading files.
272/// Default option by [FileStructure]
273struct StaticFileProvider {}
274
275impl WatchProvider for StaticFileProvider {
276    fn watch(&mut self) -> Result<(), Error> {
277        Ok(())
278    }
279
280    fn set_data(&mut self, _data: Arc<RwLock<HashMap<String, String>>>) -> Result<(), Error> {
281        Ok(())
282    }
283}
284
285/// Holder for localization map.
286pub struct InternationalCore {
287    holders: HashMap<String, Holder>,
288}
289
290/// Additional library, use features = ["incl_dir"] to enable.
291/// Helps to include static files in the project that will not change.
292/// See for example 'eu_ru_localization_incl_dir.rs'
293///
294/// # Examples
295///
296/// ```
297/// use include_dir::Dir;
298/// use sorrow_i18n::InternationalCore;
299/// const PROJECT_DIR: Dir = include_dir!("resources/en_ru");
300/// fn main() {
301///     let core = InternationalCore::from(PROJECT_DIR);
302///     let locale_holder = core.get_by_locale("my_locale").unwrap();
303/// }
304/// ```
305#[cfg(feature = "incl_dir")]
306impl<'a> From<Dir<'a>> for InternationalCore {
307    fn from(dir: Dir) -> Self {
308        let files = dir.files();
309        let mut msg_holder = HashMap::new();
310        // Folder is not required if files include in project.
311        // Setting default watcher by StaticFileProvider immediately.
312        for file in files {
313            let content = std::str::from_utf8(file.contents()).unwrap();
314            let structure = load_struct_from_str(content, None).unwrap();
315            let cl_struct = Arc::clone(&structure.provider);
316            let provider = cl_struct.lock();
317            match provider {
318                Ok(mut provider) => {
319                    *provider = Box::new(StaticFileProvider {});
320                }
321                Err(_e) => {
322                    panic!("Update provider by file has been failed. Poison mutex status.");
323                }
324            }
325            msg_holder.insert(structure.locale.clone(), structure);
326        };
327        InternationalCore {
328            holders: msg_holder
329        }
330    }
331}
332
333impl InternationalCore {
334    /// Creating new instance of InternationalCore.
335    ///
336    /// # Example
337    /// ```
338    /// use sorrow_i18n::InternationalCore;
339    /// let core = InternationalCore::new("folder/locales");
340    /// ```
341    /// If the file generates an error [Error::NotSupportedFileExtension], it will be skipped.
342    /// The rest of the errors cause panic.
343    pub fn new<S: Into<String>>(folder: S) -> InternationalCore {
344        let folder = folder.into();
345        let dir = std::fs::read_dir(&folder)
346            .map_err(|e| {
347                log::error!("{}", &e);
348                Error::IoError { path: folder, cause: e.to_string() }
349            }).unwrap();
350        let mut msg_holder = HashMap::new();
351
352        for path in dir {
353            let full_path = path.unwrap().path().to_str().unwrap().to_string();
354            let holder = Holder::new(full_path);
355            match holder {
356                Ok(mut holder) => {
357                    holder.watch().unwrap();
358                    msg_holder.insert(holder.locale.clone(), holder);
359                }
360                Err(err) => {
361                    match err {
362                        Error::NotSupportedFileExtension { path } => {
363                            log::trace!("Skipped {}, file is not supported .yml/.yaml extension.", path);
364                            continue;
365                        }
366                        e => {
367                            panic!("Error while loading file. {:?}", e)
368                        }
369                    }
370                }
371            }
372        }
373        InternationalCore { holders: msg_holder }
374    }
375
376    /// Get a mutable link to your localization. If no localization is found, you will get `None`.
377    pub fn get_by_locale(&self, locale: &str) -> Option<Data> {
378        let holders = &self.holders;
379        let holder = holders.get(locale)?;
380        Some(Data::new(Arc::clone(&holder.messages)))
381    }
382
383    /// Get a mutable link to your system localization. If no localization is found, you will get `None`.
384    pub fn get_current_locale(&self) -> Option<Data> {
385        let locale = get_current_locale_or_default();
386        self.get_by_locale(&*locale)
387    }
388
389    /// Get unmodifiable values (UnWatch). Perfect for localizations built into the project, due to which you get a small wrapper on `HashMap`.
390    /// If no localization is found, you will get `None`.
391    pub fn get_by_locale_state(&self, locale: &str) -> Option<UnWatchData> {
392        let holders = &self.holders;
393        let holder = holders.get(locale)?;
394        let read_state = holder.messages.read().unwrap();
395        Some(UnWatchData::new(&read_state))
396    }
397
398    /// Get unmodifiable values (UnWatch). Perfect for localizations built into the project, due to which you get a small wrapper on `HashMap`.
399    /// If no localization is found, you will get `None`. If a localization is found, then returns the current system localization.
400    pub fn get_current_locale_state(&self) -> Option<UnWatchData> {
401        let locale = get_current_locale_or_default();
402        let state = self.get_by_locale_state(&*locale)?;
403        Some(state)
404    }
405
406    /// Overrides the current provider for your localization. Implementation example: `examples/custom_provider.rs`
407    pub fn add_provider(&mut self, locale: &str, provider: Box<dyn WatchProvider + 'static + Sync + Send>) -> Result<(), Error> {
408        let holder = self.holders.get(locale);
409        match holder {
410            None => {
411                log::warn!("The provider has not been added. The locale to which you tried to add the provider does not exist.");
412                return Err(Error::ProviderNotAddedError { locale: locale.to_string(), cause: "locale not found.".to_string() });
413            }
414            Some(holder) => {
415                let guard = holder.provider.lock();
416                match guard {
417                    Ok(mut pr) => {
418                        *pr = provider;
419                        pr.set_data(Arc::clone(&holder.messages))?;
420                        pr.watch()?;
421                    }
422                    Err(_e) => {
423                        log::error!("Failed to update provider. Mutex on provider has been poison.");
424                        panic!("Poison mutex on add provider.");
425                    }
426                }
427            }
428        }
429        Ok(())
430    }
431
432    /// Add locale with custom locale holder
433    pub fn add_locale(&mut self, locale: &str, locale_holder: Holder) -> Result<(), Error> {
434        let holder = self.holders.get(locale);
435        return if holder.is_some() {
436            Err(Error::DuplicateLocale { locale: locale.to_string() })
437        } else {
438            self.holders.insert(locale.to_string(), locale_holder);
439            Ok(())
440        };
441    }
442}
443
444/// Getting data by holder's.
445pub trait GetData {
446    /// Getting locale message by key. If key does not exist, return [Option::None].
447    ///
448    /// # Examples
449    ///
450    /// ```
451    /// use sorrow_i18n::{GetData, InternationalCore};
452    /// let i18n = InternationalCore::new("locale");
453    /// let en = i18n.get_by_locale("en").unwrap();
454    /// let my_data = en.get("my_data");
455    ///
456    /// match my_data {
457    ///     None => {
458    ///         panic!("No found my_data key.")
459    ///     }
460    ///     Some(k) => {
461    ///         println!("Found key my_data, value: {}", &k)
462    ///     }
463    /// }
464    /// ```
465    fn get<S: AsRef<str>>(&self, key: S) -> Option<String>;
466
467    /// Getting locale message by key. If key does not exist, return `key`.
468    ///
469    /// # Examples
470    ///
471    /// ```
472    /// use sorrow_i18n::{GetData, InternationalCore};
473    /// let i18n = InternationalCore::new("locale");
474    /// let en = i18n.get_by_locale("en").unwrap();
475    /// // If data is not found ref my_data == "my_data"
476    /// // Else you getting data.
477    /// let my_data = en.get_or_default("my_data");
478    /// ```
479    fn get_or_default<S: AsRef<str>>(&self, key: S) -> String;
480
481    /// Getting all keys in holder's
482    ///
483    /// # Examples
484    ///
485    /// ```
486    /// use sorrow_i18n::{GetData, InternationalCore};
487    /// let i18n = InternationalCore::new("locale");
488    /// let en = i18n.get_by_locale("en").unwrap();
489    /// let keys = en.keys();
490    /// // Print all key's in en locale holder.
491    /// keys.iter().for_each(|k| println!("{}", k));
492    /// ```
493    fn keys(&self) -> Vec<String>;
494}
495
496/// Works with an ordinary hash map, useful when the data never changes.
497/// It's simple wrapper.
498pub struct UnWatchData {
499    holder: HashMap<String, String>,
500}
501
502impl UnWatchData {
503    /// Creating [UnWatchData] by reference for original data.
504    pub fn new(holder: &HashMap<String, String>) -> Self {
505        UnWatchData {
506            holder: holder.clone()
507        }
508    }
509}
510
511impl GetData for UnWatchData {
512    fn get<S: AsRef<str>>(&self, key: S) -> Option<String> {
513        return self.holder.get(key.as_ref()).map(|r| r.to_string());
514    }
515
516    fn get_or_default<S: AsRef<str>>(&self, key: S) -> String {
517        return self.get(key.as_ref()).unwrap_or_else(|| key.as_ref().to_string());
518    }
519
520    fn keys(&self) -> Vec<String> {
521        self.holder.keys().map(|k| k.to_string()).collect::<Vec<String>>()
522    }
523}
524
525/// We work with a mutable data ref.
526pub struct Data {
527    holder: Arc<RwLock<HashMap<String, String>>>,
528}
529
530impl Data {
531    /// Creating [Data] by reference for original data. (mutable)
532    pub fn new(holder: Arc<RwLock<HashMap<String, String>>>) -> Self {
533        Data {
534            holder: Arc::clone(&holder)
535        }
536    }
537}
538
539impl GetData for Data {
540    fn get<S: AsRef<str>>(&self, key: S) -> Option<String> {
541        let state = self.holder.read().unwrap();
542        return state.get(key.as_ref()).map(|r| r.to_string());
543    }
544
545    fn get_or_default<S: AsRef<str>>(&self, key: S) -> String {
546        return match self.holder.read().unwrap().get(key.as_ref()) {
547            None => {
548                key.as_ref().to_string().clone()
549            }
550            Some(v) => {
551                v.clone()
552            }
553        };
554    }
555
556    fn keys(&self) -> Vec<String> {
557        self.holder.read().unwrap().keys().map(|k| k.to_string()).collect::<Vec<String>>()
558    }
559}
560
561/// The simplest information keeper.
562/// Contains a link to the data itself that can be dynamically updated.
563/// The locale for determining what this state refers to.
564/// And also, the provider who is responsible for the volatility of the data.
565pub struct Holder {
566    messages: Arc<RwLock<HashMap<String, String>>>,
567    locale: String,
568    provider: Arc<Mutex<Box<dyn WatchProvider + Sync + Send>>>,
569}
570
571impl Holder {
572    /// Return [Holder]
573    ///
574    /// # Arguments
575    ///
576    /// * `path` - folder by localization's.
577    ///
578    /// # Examples
579    /// ```
580    /// use sorrow_i18n::Holder;
581    /// let holder = Holder::new("my_locale_folder");
582    /// ```
583    pub fn new<S: Into<String>>(path: S) -> Result<Holder, Error> {
584        load_struct(path)
585    }
586}
587
588impl WatchProvider for Holder {
589    fn watch(&mut self) -> Result<(), Error> {
590        self.provider.lock().unwrap().watch()
591    }
592
593    fn set_data(&mut self, data: Arc<RwLock<HashMap<String, String>>>) -> Result<(), Error> {
594        self.messages = data;
595        Ok(())
596    }
597}
598
599enum FileData {
600    Map(HashMap<String, FileData>),
601    String(String),
602}
603
604/// Default structure by file localization.
605///
606/// #Examples
607///
608/// Basic usage:
609///
610/// ```yaml
611/// kind: I18N
612/// locale: EE
613/// description: test en
614/// data:
615///   name: "Helly belly"
616/// ```
617#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
618pub struct FileStructure {
619    /// Kind - I18N. It is necessary to understand if the file does not belong to the localization category.
620    kind: String,
621
622    /// Locale - file locale type.
623    locale: String,
624
625    /// Description - for user, optional parameter.
626    description: Option<String>,
627
628    /// Provider - optional parameter, if is None, [StaticFileProvider]. For additional information see [Providers].
629    provider: Option<Providers>,
630
631    /// Data - localization information. Format key-value, optional.
632    #[serde(flatten)]
633    data: Option<Value>,
634}
635
636/// Loading [FileStructure], and creating [Holder].
637/// If structure is invalid [Error::InvalidStructure]
638/// If structure is valid, but kind is not valid, return: [Error::InvalidHeader]
639/// Path - optional if use static provider with [incl_dir] `features`.
640fn load_struct_from_str(data: &str, path: Option<String>) -> Result<Holder, Error> {
641    let messages = Arc::new(RwLock::new(HashMap::new()));
642    let path = path.unwrap_or_default();
643    let structure: FileStructure = serde_yaml::from_str(data).map_err(|e| Error::InvalidStructure { path: path.clone(), cause: e.to_string() })?;
644
645    if structure.kind.ne("I18N") {
646        log::error!("Invalid header for file: {}. Expected: I18N.", &path);
647        return Err(Error::InvalidHeader { path: path.clone() });
648    };
649
650    log::trace!("Loading structure by path: {}.\nDescription: {:?}\nLocale: {}", &path, &structure.description, &structure.locale);
651
652    let locale = structure.locale.clone();
653
654    match structure.data {
655        None => {
656            log::warn!("Empty data for {} locale. File path: {}.", &structure.locale, &*path);
657        }
658        Some(kv) => {
659            messages
660                .write()
661                .and_then(|mut m| {
662                    m.extend(to_flatten(String::default(), FileData::from(kv)));
663                    Ok(())
664                }).unwrap();
665        }
666    };
667
668    return match structure.provider {
669        None => {
670            // Unwatch if provider is not exists
671            Ok(Holder {
672                messages,
673                locale,
674                provider: Arc::new(Mutex::new(Box::new(StaticFileProvider {}))),
675            })
676        }
677        Some(p) => {
678            match p {
679                Providers::FileProvider => {
680                    let provider = FileProvider::new(Arc::clone(&messages), path.clone());
681                    Ok(Holder {
682                        messages,
683                        locale,
684                        provider: Arc::new(Mutex::new(Box::new(provider))),
685                    })
686                }
687                Providers::StaticFileProvider => {
688                    Ok(Holder {
689                        messages,
690                        locale,
691                        provider: Arc::new(Mutex::new(Box::new(StaticFileProvider {}))),
692                    })
693                }
694            }
695        }
696    };
697}
698
699/// Load file ant trigger loading [FileStructure] by [load_struct_from_str()]
700/// If file extension is not .yaml or .yml, the error is hit [Error::NotSupportedFileExtension]
701/// Another error, if IO operation has been failed. [Error::IoError]
702fn load_struct<S: Into<String>>(path: S) -> Result<Holder, Error> {
703    let mut data = String::new();
704    let path = path.into().trim_end().to_string();
705
706    if !path.ends_with(".yaml") && !path.ends_with(".yml") {
707        return Err(Error::NotSupportedFileExtension { path: path.clone() });
708    }
709
710    let mut file = File::open(&path)
711        .map_err(|e| {
712            log::error!("Error while open file {}. Additional information: {}", &path, &e);
713            Error::IoError {
714                path: path.clone(),
715                cause: e.to_string(),
716            }
717        })?;
718    file.read_to_string(&mut data).unwrap();
719    load_struct_from_str(&*data, Some(path))
720}
721
722/// Getting locale or default by `locale` parameter with `sys-locale` library.
723fn get_locale_or_default(locale: &str) -> String {
724    get_locale().unwrap_or(String::from(locale))
725}
726
727/// Get current system locale or return default `en-US`
728fn get_current_locale_or_default() -> String {
729    get_locale_or_default("en-US")
730}
731
732impl From<serde_yaml::Value> for FileData {
733    fn from(value: serde_yaml::Value) -> Self {
734        match value {
735            serde_yaml::Value::Mapping(obj) => FileData::Map(
736                obj.into_iter()
737                    .filter_map(|(k, v)| match k {
738                        serde_yaml::Value::String(s) => Some((s, FileData::from(v))),
739                        _ => None,
740                    })
741                    .collect(),
742            ),
743            serde_yaml::Value::String(s) => FileData::String(s),
744            _ => FileData::Map(Default::default()),
745        }
746    }
747}
748
749fn to_flatten(name: String, val: FileData) -> HashMap<String, String> {
750    let mut map = HashMap::new();
751    match val {
752        FileData::Map(array) => {
753            for (name2, v) in array.into_iter() {
754                map.extend(to_flatten(
755                    if name.is_empty() {
756                        name2
757                    } else {
758                        format!("{}.{}", name, name2)
759                    },
760                    v,
761                ));
762            }
763        }
764        FileData::String(s) => {
765            map.insert(name, s.clone());
766        }
767    };
768    map
769}