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}