yaml_config/lib.rs
1pub mod error;
2
3pub use crate::error::ParseError;
4
5use enum_as_inner::EnumAsInner;
6use fxhash::FxBuildHasher;
7use indexmap::IndexMap;
8use linked_hash_map::LinkedHashMap;
9use std::env;
10use std::fs::read_to_string;
11use yaml_rust::{Yaml, YamlLoader};
12
13/// Defines the preference for loading of a configuration when a variable exists in the
14/// YAML and also along the same path in the environment.
15#[derive(Debug, PartialEq, Eq)]
16pub enum Preference {
17 PreferYaml,
18 PreferEnv,
19}
20
21/// A wrapped type enum useful for allowing polymorphic returns from
22/// the map creation function.
23///
24/// # Examples
25///
26/// fn main() {
27///
28/// ```rust
29/// use yaml_config::Value;
30/// let x = Value::I32(10);
31/// let val = *x.as_i32().unwrap();
32/// ```
33/// }
34#[derive(Debug, EnumAsInner)]
35pub enum Value {
36 I32(i32),
37 I64(i64),
38 F32(f32),
39 F64(f64),
40 String(String),
41 Bool(bool),
42}
43
44/// Provides a simple way to allow question mark syntax in order to
45/// convert environment errors into ParseErrors.
46fn env_or_error(key: &str) -> Result<String, ParseError> {
47 match env::var_os(key) {
48 Some(v) => Ok(v
49 .into_string()
50 .expect("Could not convert OsString into string.")),
51 None => {
52 let msg = format!("Error parsing OS environment variable for {}", key);
53 Err(ParseError {
54 module: "std::env".to_string(),
55 message: msg,
56 })
57 }
58 }
59}
60
61/// Takes a key and a Yaml reference, parses it, and sets the key.
62///
63/// In addition to doing the initial parsing it will also do environment finding. If a given
64/// key is null, or `prefer_env` is true, then it will search the environment for the given
65/// key string and attempt to use that key string's value.
66///
67fn maybe_yaml_to_value(
68 key: &str,
69 maybe_val: &Yaml,
70 prefer_env: bool,
71 map: &mut IndexMap<String, Value, FxBuildHasher>,
72) -> Result<(), ParseError> {
73 if maybe_val.is_null() {
74 // Because the value is null we have to attempt a full parse of whatever is coming back
75 // from the user's environment since we don't have an indicator from the YAML itself.
76 let val_str = env_or_error(key)?;
77
78 let val = match val_str.parse::<i64>() {
79 Ok(v) => Value::I64(v),
80 Err(_) => match val_str.parse::<f64>() {
81 Ok(v) => Value::F64(v),
82 Err(_) => match val_str.parse::<bool>() {
83 Ok(v) => Value::Bool(v),
84 Err(_) => Value::String(val_str),
85 },
86 },
87 };
88
89 map.insert(key.to_string(), val);
90 return Ok(());
91 }
92
93 if maybe_val.as_str().is_some() {
94 if prefer_env {
95 match env_or_error(key) {
96 Ok(v) => {
97 map.insert(key.to_string(), Value::String(v));
98 }
99 Err(_) => {
100 map.insert(
101 key.to_string(),
102 Value::String(maybe_val.as_str().unwrap().to_string()),
103 );
104 }
105 };
106 } else {
107 map.insert(
108 key.to_string(),
109 Value::String(maybe_val.as_str().unwrap().to_string()),
110 );
111 }
112
113 return Ok(());
114 }
115
116 if maybe_val.as_i64().is_some() {
117 if prefer_env {
118 match env_or_error(key) {
119 Ok(v) => {
120 let e_val = v.parse::<i64>().unwrap();
121 map.insert(key.to_string(), Value::I64(e_val));
122 }
123 Err(_) => {
124 map.insert(key.to_string(), Value::I64(maybe_val.as_i64().unwrap()));
125 }
126 };
127 } else {
128 map.insert(key.to_string(), Value::I64(maybe_val.as_i64().unwrap()));
129 }
130
131 return Ok(());
132 }
133
134 if maybe_val.as_bool().is_some() {
135 if prefer_env {
136 match env_or_error(key) {
137 Ok(v) => {
138 let e_val = v.parse::<bool>().unwrap();
139 map.insert(key.to_string(), Value::Bool(e_val));
140 }
141 Err(_) => {
142 map.insert(key.to_string(), Value::Bool(maybe_val.as_bool().unwrap()));
143 }
144 };
145 } else {
146 map.insert(key.to_string(), Value::Bool(maybe_val.as_bool().unwrap()));
147 }
148
149 return Ok(());
150 }
151
152 if maybe_val.as_f64().is_some() {
153 if prefer_env {
154 match env_or_error(key) {
155 Ok(v) => {
156 let e_val = v.parse::<f64>().unwrap();
157 map.insert(key.to_string(), Value::F64(e_val));
158 }
159 Err(_) => {
160 map.insert(key.to_string(), Value::F64(maybe_val.as_f64().unwrap()));
161 }
162 };
163 } else {
164 map.insert(key.to_string(), Value::F64(maybe_val.as_f64().unwrap()));
165 }
166
167 Ok(())
168 } else {
169 let msg = format!("Failed to convert type for {}", key);
170 Err(ParseError {
171 module: "config".to_string(),
172 message: msg,
173 })
174 }
175}
176
177/// Converts a YAML key into a string for processing.
178fn key_string(key: &Yaml) -> Result<&str, ParseError> {
179 match key.as_str() {
180 Some(s) => Ok(s),
181 None => Err(ParseError {
182 module: "config".to_string(),
183 message: format!("Could not convert key {:?} into String.", key),
184 }),
185 }
186}
187
188/// Recursive map builder.
189///
190/// Given a "root" of the yaml file it will generate a configuration recursively. Due
191/// to it's use of recursion the actual depth of the YAML file is limited to the depth of
192/// the stack. But given most (arguably 99.9%) of YAML files are not even 5 levels deep
193/// this seemed like an acceptable trade off for an easier to write algorithm.
194///
195/// Effectively, this performs a depth first search of the YAML file treating each top level
196/// feature as a tree with 1-to-N values. When a concrete (non-hash) value is arrived at
197/// the builder constructs a depth-based string definining it.
198///
199/// The arguments enforce an `FxBuildHasher` based `IndexMap` to insure extremely fast
200/// searching of the map. *this map is modified in place*.
201///
202/// # Arguments
203///
204/// * `root` - The start of the YAML document as given by `yaml-rust`.
205/// * `config` - An IndexMap of String -> Value. It must use an FxBuilderHasher.
206/// * `prefer_env` - When `true` will return an environment variable matching the path string
207/// regardless of whether the YAML contains a value for this key. It will prefer
208/// the given value otherwise unless that value is `null`.
209/// * `current_key_str` - An optional argument that stores the current string of the path.
210///
211fn build_map(
212 root: &LinkedHashMap<Yaml, Yaml>,
213 config: &mut IndexMap<String, Value, FxBuildHasher>,
214 prefer_env: bool,
215 current_key_str: Option<&str>,
216) -> Result<(), ParseError> {
217 // Recursively parse each root key to resolve.
218 for key in root.keys() {
219 let maybe_val = &root[key];
220
221 let key_str = match current_key_str {
222 Some(k) => {
223 // In this case we have a previous value.
224 // We need to construct the current depth-related key.
225 let mut next_key = k.to_uppercase().to_string();
226 next_key.push('_');
227 next_key.push_str(&key_string(key)?.to_uppercase());
228 next_key
229 }
230 None => key_string(key)?.to_uppercase().to_string(),
231 };
232
233 if maybe_val.is_array() {
234 return Err(ParseError {
235 module: "config::build_map".to_string(),
236 message: "Arrays are currently unsupported for configuration.".to_string(),
237 });
238 }
239
240 if maybe_val.as_hash().is_none() {
241 // Base condition
242 maybe_yaml_to_value(&key_str.to_uppercase(), maybe_val, prefer_env, config)?;
243 } else {
244 // Now we need to construct the key for one layer deeper.
245 build_map(
246 maybe_val.as_hash().unwrap(),
247 config,
248 prefer_env,
249 Some(&key_str),
250 )?;
251 }
252 }
253
254 Ok(())
255}
256
257/// Loads a configuration file.
258///
259/// The parser will first load the YAML file. It then re-organizes the YAML
260/// file into a common naming convention. Given:
261///
262/// ```yaml
263/// X:
264/// y: "value"
265/// ```
266///
267/// The key will be `X_Y` and the value will be the string `"value"`.
268///
269/// After loading, it investigates each value looking for nulls. In the
270/// case of a null, it will search the environment for the
271/// key (in the above example `X_Y`). If found, it replaces the value.
272/// If not found, it will error.
273///
274/// In the event that a key in the environment matches a key that is
275/// provided in the YAML it will prefer the key in the YAML file. To
276/// override this, pass a `Some(Preference::PreferEnv)` to the
277/// `preference` argument.
278///
279/// The resulting `IndexMap` will have string keys representing the path
280/// configuration described above, and values that are contained in the `Value`
281/// enum. See the documentation for `config::Value` for more information on
282/// usage.
283///
284/// # Arguments
285///
286/// * `file_path` - A string representing the path to the YAML file.
287/// * `preference` - The preference for handling values when a key has a value in the
288///
289/// # Examples
290///
291/// ```rust
292/// use yaml_config::load;
293/// let configuration = load("path/to/yaml/file.yaml", None);
294///
295/// ```
296///
297/// Use with preference:
298///
299/// ```rust
300/// use yaml_config::Preference;
301/// use yaml_config::load;
302/// let configuration = load("path/to/yaml/file.yaml",
303/// Some(Preference::PreferEnv));
304/// ```
305pub fn load(
306 file_path: &str,
307 preference: Option<Preference>,
308) -> Result<IndexMap<String, Value, FxBuildHasher>, ParseError> {
309 let prefer_env = match preference {
310 Some(p) => p == Preference::PreferEnv,
311 None => false,
312 };
313 let doc_str = read_to_string(file_path)?;
314 let yaml_docs = YamlLoader::load_from_str(&doc_str)?;
315 let base_config = &yaml_docs[0];
316 let user_config = match base_config.as_hash() {
317 Some(hash) => hash,
318 None => {
319 return Err(ParseError {
320 module: "config".to_string(),
321 message: "Failed to parse YAML as hashmap.".to_string(),
322 })
323 }
324 };
325
326 let mut config = IndexMap::with_hasher(FxBuildHasher::default());
327
328 build_map(user_config, &mut config, prefer_env, None)?;
329
330 Ok(config)
331}
332
333#[cfg(test)]
334mod test;