Skip to main content

config/
yaml.rs

1use crate::{pascal_case, path, Error, FileSource, Result, Settings};
2use std::fs;
3use tokens::{ChangeToken, FileChangeToken, NeverChangeToken};
4use yaml_rust2::{yaml::Hash, Yaml, YamlLoader};
5
6struct YamlVisitor<'a> {
7    settings: &'a mut Settings,
8    paths: Vec<String>,
9}
10
11impl<'a> YamlVisitor<'a> {
12    #[inline]
13    fn new(settings: &'a mut Settings) -> Self {
14        Self {
15            settings,
16            paths: Vec::new(),
17        }
18    }
19}
20
21impl YamlVisitor<'_> {
22    #[inline]
23    fn visit(mut self, root: &Hash) {
24        self.visit_hash(root)
25    }
26
27    fn visit_hash(&mut self, hash: &Hash) {
28        if hash.is_empty() {
29            if let Some(key) = self.paths.last() {
30                self.settings.insert(pascal_case(key), String::new());
31            }
32        } else {
33            for (name, value) in hash {
34                let key = match name {
35                    Yaml::String(s) => pascal_case(s),
36                    Yaml::Integer(i) => i.to_string(),
37                    Yaml::Real(s) => s.clone(),
38                    Yaml::Boolean(b) => b.to_string(),
39                    _ => String::new(),
40                };
41                self.enter_context(key);
42                self.visit_value(value);
43                self.exit_context();
44            }
45        }
46    }
47
48    fn visit_value(&mut self, value: &Yaml) {
49        match value {
50            Yaml::Hash(ref hash) => self.visit_hash(hash),
51            Yaml::Array(array) => {
52                for (index, element) in array.iter().enumerate() {
53                    self.enter_context(index.to_string());
54                    self.visit_value(element);
55                    self.exit_context();
56                }
57            }
58            Yaml::String(value) => self.add_value(value),
59            Yaml::Integer(value) => self.add_value(value),
60            Yaml::Real(value) => self.add_value(value),
61            Yaml::Boolean(value) => self.add_value(value),
62            Yaml::Null => self.add_value(String::new()),
63            Yaml::Alias(_) | Yaml::BadValue => self.add_value(String::new()),
64        }
65    }
66
67    fn add_value<T: ToString>(&mut self, value: T) {
68        let key = self.paths.last().expect("no paths");
69        self.settings.insert(pascal_case(key), value.to_string());
70    }
71
72    fn enter_context(&mut self, context: String) {
73        if self.paths.is_empty() {
74            self.paths.push(context);
75        } else {
76            self.paths
77                .push(path::combine(&[&self.paths[self.paths.len() - 1], &context]));
78        }
79    }
80
81    #[inline]
82    fn exit_context(&mut self) {
83        self.paths.pop();
84    }
85}
86
87/// Represents a [configuration provider](crate::Provider) for `*.yaml` and `*.yml` files.
88pub struct Provider(FileSource);
89
90impl Provider {
91    /// Initializes a new `*.yaml` file configuration provider.
92    ///
93    /// # Arguments
94    ///
95    /// * `file` - The `*.yaml` [file source](FileSource) information
96    #[inline]
97    pub fn new(file: FileSource) -> Self {
98        Self(file)
99    }
100}
101
102impl crate::Provider for Provider {
103    #[inline]
104    fn name(&self) -> &str {
105        path::provider(&self.0.path, "Yaml")
106    }
107
108    fn reload_token(&self) -> Box<dyn ChangeToken> {
109        if self.0.reload_on_change {
110            Box::new(FileChangeToken::new(self.0.path.clone()))
111        } else {
112            Box::new(NeverChangeToken)
113        }
114    }
115
116    fn load(&self, settings: &mut Settings) -> Result {
117        if !self.0.path.is_file() {
118            if self.0.optional {
119                return Ok(());
120            } else {
121                return Err(Error::MissingFile(self.0.path.clone()));
122            }
123        }
124
125        let content = fs::read_to_string(&self.0.path).map_err(Error::unknown)?;
126        let docs = YamlLoader::load_from_str(&content).map_err(|e| Error::InvalidFile {
127            message: e.to_string(),
128            path: self.0.path.clone(),
129        })?;
130
131        if docs.is_empty() {
132            return Ok(());
133        }
134
135        let doc = &docs[0];
136
137        let Yaml::Hash(ref hash) = doc else {
138            return Err(Error::InvalidFile {
139                message: format!(
140                    "Top-level YAML element must be a mapping, but '{}' was found.",
141                    match doc {
142                        Yaml::Array(_) => "array",
143                        Yaml::String(_) => "string",
144                        Yaml::Integer(_) => "integer",
145                        Yaml::Real(_) => "float",
146                        Yaml::Boolean(_) => "Boolean",
147                        Yaml::Null => "null",
148                        _ => "unknown",
149                    }
150                ),
151                path: self.0.path.clone(),
152            });
153        };
154
155        YamlVisitor::new(settings).visit(hash);
156        Ok(())
157    }
158}