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
87pub struct Provider(FileSource);
89
90impl Provider {
91 #[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}