dotfiles_core/yaml_util.rs
1// Copyright (c) 2021-2022 Miguel Barreto and others
2//
3// Permission is hereby granted, free of charge, to any person obtaining
4// a copy of this software and associated documentation files (the
5// "Software"), to deal in the Software without restriction, including
6// without limitation the rights to use, copy, modify, merge, publish,
7// distribute, sublicense, and/or sell copies of the Software, and to
8// permit persons to whom the Software is furnished to do so, subject to
9// the following conditions:
10//
11// The above copyright notice and this permission notice shall be
12// included in all copies or substantial portions of the Software.
13//
14// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
22//! Module that defines helper functions to process YAML configuration sources.
23extern crate strict_yaml_rust;
24
25use std::{path::Path, str::FromStr, vec};
26
27use crate::{
28 error::{fold_until_first_err, DotfilesError, ErrorType},
29 settings::{parse_setting, Setting, Settings},
30};
31use strict_yaml_rust::{StrictYaml, StrictYamlLoader};
32
33/// Executes the `process_function` on each of the items in the `yaml_hash`. The yaml hash is
34/// assumed to be string keyed. It stops execution if any of the process functions returns an Error,
35/// and returns said error.
36pub fn process_yaml_hash_until_first_err<F>(
37 yaml_hash: &StrictYaml,
38 mut process_function: F,
39) -> Result<(), DotfilesError>
40where
41 F: FnMut(String, &StrictYaml) -> Result<(), DotfilesError>,
42{
43 if let StrictYaml::Hash(hash) = yaml_hash {
44 hash.into_iter().try_for_each(|(key, value)| {
45 parse_as_string(key)
46 .map(|key_str| (key_str, value))
47 .and_then(|(key, val)| process_function(key, val))
48 })
49 } else {
50 Err(DotfilesError::from_wrong_yaml(
51 "Expected a yaml hash, got something else".to_owned(),
52 yaml_hash.to_owned(),
53 StrictYaml::Hash(Default::default()),
54 ))
55 }
56}
57
58/// Gets the value for a specified key in a yaml hash and does something with it.
59///
60/// Returns the result of the process function being applied to the value in question.
61///
62/// # Errors
63///
64/// * Will return an error that happens during the process_function application.
65/// * Will return a [ErrorType::UnexpectedYamlTypeError] if the value is not a hash or if the hash
66/// doesn't have string-based keys.
67/// * Will return a [ErrorType::IncompleteConfigurationError] if the key is not found in the hash.
68pub fn process_value_from_yaml_hash<T, F>(
69 key: &str,
70 yaml_hash: &StrictYaml,
71 mut process: F,
72) -> Result<T, DotfilesError>
73where
74 F: FnMut(&StrictYaml) -> Result<T, DotfilesError>,
75{
76 if let StrictYaml::Hash(inner_hash) = yaml_hash {
77 match inner_hash.get(&StrictYaml::String(key.into())) {
78 Some(yaml) => process(yaml),
79 None => Err(DotfilesError::from(
80 format!("Hash does not contain key {}", key.to_owned()),
81 ErrorType::IncompleteConfigurationError {
82 missing_field: key.into(),
83 },
84 )),
85 }
86 } else {
87 Err(DotfilesError::from_wrong_yaml(
88 "process_value_from_yaml_hash expects a hash, but got something else".into(),
89 yaml_hash.to_owned(),
90 StrictYaml::Hash(Default::default()),
91 ))
92 }
93}
94
95/// Calls a processing function on all elements of an array, will fail if any of the elements fail
96/// to process.
97///
98/// # Errors
99///
100/// * Any error that happens in the processing function.
101/// * [ErrorType::UnexpectedYamlTypeError] if the yaml passed is not an array
102pub fn map_yaml_array<T, F>(yaml_array: &StrictYaml, process: F) -> Result<Vec<T>, DotfilesError>
103where
104 F: FnMut(&StrictYaml) -> Result<T, DotfilesError>,
105{
106 if let StrictYaml::Array(inner_vec) = yaml_array {
107 inner_vec.iter().map(process).collect()
108 } else {
109 Err(DotfilesError::from_wrong_yaml(
110 "map_yaml_array expects a yaml array, but got something else".into(),
111 yaml_array.to_owned(),
112 StrictYaml::Array(vec![]),
113 ))
114 }
115}
116
117/// Gets a specific setting from a yaml hash
118///
119/// # Errors
120/// * [ErrorType::UnexpectedYamlTypeError] if the yaml is not a hash.
121/// * [ErrorType::UnexpectedYamlTypeError] if the yaml does not have string keys.
122/// * [ErrorType::UnexpectedYamlTypeError] if the value's type does not match the `setting_type`.
123/// * [ErrorType::IncompleteConfigurationError] if the hash does not contain the requested key
124pub fn get_setting_from_yaml_hash(
125 name: &str,
126 setting_type: &Setting,
127 yaml: &StrictYaml,
128) -> Result<Setting, DotfilesError> {
129 process_value_from_yaml_hash(name, yaml, |value_for_name| {
130 parse_setting(setting_type, value_for_name)
131 })
132}
133/// Gets a specific boolean setting from a yaml hash
134///
135/// # Errors
136/// * [ErrorType::UnexpectedYamlTypeError] if the yaml is not a hash.
137/// * [ErrorType::UnexpectedYamlTypeError] if the yaml does not have string keys.
138/// * [ErrorType::UnexpectedYamlTypeError] if the value's type is not boolean.
139/// * [ErrorType::IncompleteConfigurationError] if the hash does not contain the requested key
140pub fn get_boolean_from_yaml_hash(name: &str, yaml: &StrictYaml) -> Result<bool, DotfilesError> {
141 process_value_from_yaml_hash(name, yaml, |value_for_name| {
142 parse_as_boolean(value_for_name)
143 })
144}
145
146/// Gets a specific integer setting from a yaml hash
147///
148/// # Errors
149/// * [ErrorType::UnexpectedYamlTypeError] if the yaml is not a hash.
150/// * [ErrorType::UnexpectedYamlTypeError] if the yaml does not have string keys.
151/// * [ErrorType::UnexpectedYamlTypeError] if the value's type is not integer.
152/// * [ErrorType::IncompleteConfigurationError] if the hash does not contain the requested key
153pub fn get_integer_from_yaml_hash(name: &str, yaml: &StrictYaml) -> Result<i64, DotfilesError> {
154 process_value_from_yaml_hash(name, yaml, |value_for_name| {
155 parse_as_integer(value_for_name)
156 })
157}
158
159/// Gets a specific string setting from a yaml hash
160///
161/// # Errors
162/// * [ErrorType::UnexpectedYamlTypeError] if the yaml is not a hash.
163/// * [ErrorType::UnexpectedYamlTypeError] if the yaml does not have string keys.
164/// * [ErrorType::UnexpectedYamlTypeError] if the value's type is not string.
165/// * [ErrorType::IncompleteConfigurationError] if the hash does not contain the requested key
166pub fn get_string_from_yaml_hash(name: &str, yaml: &StrictYaml) -> Result<String, DotfilesError> {
167 process_value_from_yaml_hash(name, yaml, parse_as_string)
168}
169
170/// Gets a specific string array setting from a yaml hash
171///
172/// # Errors
173/// * [ErrorType::UnexpectedYamlTypeError] if the yaml is not a hash.
174/// * [ErrorType::UnexpectedYamlTypeError] if the yaml does not have string keys.
175/// * [ErrorType::UnexpectedYamlTypeError] if the value's type is not array, or some of its values
176/// are not string.
177/// * [ErrorType::IncompleteConfigurationError] if the hash does not contain the requested key
178pub fn get_string_array_from_yaml_hash(
179 name: &str,
180 yaml: &StrictYaml,
181) -> Result<Vec<String>, DotfilesError> {
182 process_value_from_yaml_hash(name, yaml, |value_for_name| {
183 parse_as_string_array(value_for_name)
184 })
185}
186
187/// Gets a specific string array setting from a yaml hash, but if it is not found it returns an
188/// empty array.
189///
190/// # Errors
191/// * [ErrorType::UnexpectedYamlTypeError] if the yaml is not a hash.
192/// * [ErrorType::UnexpectedYamlTypeError] if the yaml does not have string keys.
193/// * [ErrorType::UnexpectedYamlTypeError] if the value's type is not array, or some of its values
194/// are not string.
195pub fn get_optional_string_array_from_yaml_hash(
196 name: &str,
197 yaml: &StrictYaml,
198) -> Result<Vec<String>, DotfilesError> {
199 get_string_array_from_yaml_hash(name, yaml).or_else(|err| {
200 if err.is_missing_config(name) {
201 Ok(vec![])
202 } else {
203 Err(err)
204 }
205 })
206}
207
208/// Gets a boolean value for the setting named `name`.
209///
210/// First it tries to find a value for the setting in the `context_settings`
211/// argument, if it doesn't contain any it falls back to `directive-defaults`.
212///
213/// Returns an error if no such setting was found in either setting collection.
214pub fn get_boolean_setting_from_context(
215 name: &str,
216 context_settings: &Settings,
217 directive_defaults: &Settings,
218) -> Result<bool, DotfilesError> {
219 if let Setting::Boolean(b) = get_setting_from_context(name, context_settings, directive_defaults)?
220 {
221 Ok(b)
222 } else {
223 Err(DotfilesError::from(
224 format!("Setting {name} was found in directive defaults but is not boolean",),
225 ErrorType::CoreError,
226 ))
227 }
228}
229
230/// Gets a String value for the setting named `name`.
231///
232/// First it tries to find a value for the setting in the `context_settings`
233/// argument, if it doesn't contain any it falls back to `directive-defaults`.
234///
235/// Returns an error if no such setting was found in either setting collection.
236pub fn get_string_setting(
237 name: &str,
238 context_settings: &Settings,
239 directive_defaults: &Settings,
240) -> Result<String, DotfilesError> {
241 if let Setting::String(s) = get_setting_from_context(name, context_settings, directive_defaults)?
242 {
243 Ok(s)
244 } else {
245 Err(DotfilesError::from(
246 format!("Setting {name} was found in directive defaults but is not a string",),
247 ErrorType::CoreError,
248 ))
249 }
250}
251
252/// Gets a Int value for the setting named `name`.
253///
254/// First it tries to find a value for the setting in the `context_settings`
255/// argument, if it doesn't contain any it falls back to `directive-defaults`.
256///
257/// Returns an error if no such setting was found in either setting collection.
258pub fn get_integer_setting(
259 name: &str,
260 context_settings: &Settings,
261 directive_defaults: &Settings,
262) -> Result<i64, DotfilesError> {
263 if let Setting::Integer(x) = get_setting_from_context(name, context_settings, directive_defaults)?
264 {
265 Ok(x)
266 } else {
267 Err(DotfilesError::from(
268 format!("Setting {name} was found in directive defaults but is not an integer",),
269 ErrorType::CoreError,
270 ))
271 }
272}
273
274/// Gets a String value for the setting named `name`.
275///
276/// First it tries to find a value for the setting in the `context_settings`
277/// argument, if it doesn't contain any it falls back to `directive-defaults`
278///
279/// Returns an error if no such setting was found in either setting collection.
280pub fn get_setting_from_context(
281 name: &str,
282 context_settings: &Settings,
283 directive_defaults: &Settings,
284) -> Result<Setting, DotfilesError> {
285 if let Some(setting) = context_settings.get(name) {
286 Ok(setting.clone())
287 } else if let Some(setting) = directive_defaults.get(name) {
288 Ok(setting.clone())
289 } else {
290 Err(DotfilesError::from(
291 format!("Setting {name} couldn't be found in context or defaults"),
292 ErrorType::CoreError,
293 ))
294 }
295}
296
297/// Gets a Boolean value from YAML or context.
298///
299/// First it tries to find a value in `yaml`, if it can't find one then it
300/// falls back to `context_settings` or finally `default_settings`.
301///
302/// # Errors
303/// - Found a setting in YAML but that couldn't be parsed as a boolean.
304/// - Didn't find a setting matching this name anywhere
305pub fn get_boolean_setting_from_yaml_or_context(
306 name: &str,
307 yaml: &StrictYaml,
308 context_settings: &Settings,
309 directive_defaults: &Settings,
310) -> Result<bool, DotfilesError> {
311 get_boolean_from_yaml_hash(name, yaml)
312 .or_else(|_| get_boolean_setting_from_context(name, context_settings, directive_defaults))
313}
314
315/// Gets a Integer value from YAML or context.
316///
317/// First it tries to find a value in `yaml`, if it can't find one then it
318/// falls back to `context_settings` or finally `default_settings`.
319///
320/// # Errors
321/// - Found a setting in YAML but that couldn't be parsed.
322/// - Didn't find a setting matching this name anywhere
323pub fn get_integer_setting_from_yaml_or_context(
324 name: &str,
325 yaml: &StrictYaml,
326 context_settings: &Settings,
327 directive_defaults: &Settings,
328) -> Result<i64, DotfilesError> {
329 get_integer_from_yaml_hash(name, yaml)
330 .or_else(|_| get_integer_setting(name, context_settings, directive_defaults))
331}
332
333/// Gets a String value from YAML or context.
334///
335/// First itb tries to find a value in `yaml`, if it can't find one then it
336/// falls back to `context_settings` or finally `default_settings`.
337///
338/// # Errors
339/// - Found a setting in YAML but that couldn't be parsed.
340/// - Didn't find a setting matching this name anywhere
341pub fn get_string_setting_from_yaml_or_context(
342 name: &str,
343 yaml: &StrictYaml,
344 context_settings: &Settings,
345 directive_defaults: &Settings,
346) -> Result<String, DotfilesError> {
347 process_value_from_yaml_hash(name, yaml, parse_as_string)
348 .or_else(|_| get_string_setting(name, context_settings, directive_defaults))
349}
350
351/// Gets the content of this YAML node or the value for a specific key in it.
352///
353/// If the StrictYaml node passed is a String node then it returns its contents,
354/// if the StrictYaml node is a Hash then it returns the string content of the
355/// value corresponding to the optional Key.
356///
357/// # Errors
358/// - `yaml` is neither a String nor a Hash
359/// - `yaml` is a hash but it does not contain a value for `key`
360/// - `yaml` is a hash but the value for `key` is not a String.
361pub fn get_string_content_or_keyed_value(
362 yaml: &StrictYaml,
363 key: Option<&str>,
364) -> Result<String, DotfilesError> {
365 parse_as_string(yaml).or_else(|err| {
366 if let Some(key_str) = key {
367 get_string_from_yaml_hash(key_str, yaml)
368 } else {
369 Err(err)
370 }
371 })
372}
373
374/// Gets a native `Vec<String>` from a StrictYaml::Array. It errors out if the passed yaml is not an
375/// array or if not all the items in the array are plain StrictYaml Strings
376pub fn parse_as_string_array(yaml: &StrictYaml) -> Result<Vec<String>, DotfilesError> {
377 map_yaml_array(yaml, parse_as_string)
378}
379
380/// Parse a yaml element as string, will convert booleans and integers to string if necessary.
381///
382/// # Errors
383/// * [ErrorType::UnexpectedYamlTypeError] if yaml is neither string, boolean or integer and thus
384/// cannot be parsed as string losslessly.
385pub fn parse_as_string(yaml_to_parse: &StrictYaml) -> Result<String, DotfilesError> {
386 match yaml_to_parse {
387 StrictYaml::String(s) => Ok(s.to_owned()),
388 _ => Err(DotfilesError::from_wrong_yaml(
389 "Expected StrictYaml String and got something else".into(),
390 yaml_to_parse.clone(),
391 StrictYaml::String("".into()),
392 )),
393 }
394}
395
396/// Parse a yaml element as boolean.
397///
398/// # Errors
399/// * [ErrorType::UnexpectedYamlTypeError] if yaml is not of type Boolean
400pub fn parse_as_boolean(yaml: &StrictYaml) -> Result<bool, DotfilesError> {
401 if let StrictYaml::String(b) = yaml {
402 FromStr::from_str(b).map_err(|_| {
403 DotfilesError::from_wrong_yaml(
404 format!("Got a Yaml String that can't be parsed as boolean: `{b}`"),
405 yaml.to_owned(),
406 StrictYaml::String("true".into()),
407 )
408 })
409 } else {
410 Err(DotfilesError::from_wrong_yaml(
411 "Expected StrictYaml string containing a boolean and got something else".into(),
412 yaml.clone(),
413 StrictYaml::String("false".into()),
414 ))
415 }
416}
417/// Parse a yaml element as Integer.
418///
419/// # Errors
420/// * [ErrorType::UnexpectedYamlTypeError] if yaml is not of type Integer
421pub fn parse_as_integer(yaml: &StrictYaml) -> Result<i64, DotfilesError> {
422 if let StrictYaml::String(i) = yaml {
423 FromStr::from_str(i).map_err(|_| {
424 DotfilesError::from_wrong_yaml(
425 format!("Got a Yaml String that can't be parsed as integer: `{i}`"),
426 yaml.to_owned(),
427 StrictYaml::String("11111".into()),
428 )
429 })
430 } else {
431 Err(DotfilesError::from_wrong_yaml(
432 "Expected StrictYaml String and got something else".into(),
433 yaml.clone(),
434 StrictYaml::String("0".into()),
435 ))
436 }
437}
438
439/// Parse a yaml element as an array.
440///
441/// # Errors
442/// * [ErrorType::UnexpectedYamlTypeError] if yaml is not of type array
443pub fn parse_as_array(yaml: &StrictYaml) -> Result<Vec<StrictYaml>, DotfilesError> {
444 if let Some(v) = yaml.as_vec() {
445 Ok(v.to_owned())
446 } else {
447 Err(DotfilesError::from_wrong_yaml(
448 "Expected StrictYaml Array and got something else".into(),
449 yaml.clone(),
450 StrictYaml::Array(vec![]),
451 ))
452 }
453}
454
455/// Reads a StrictYaml File. Returns Error in case of a syntax error.
456pub fn read_yaml_file(file: &Path) -> Result<Vec<StrictYaml>, DotfilesError> {
457 let contents = std::fs::read_to_string(file).map_err(DotfilesError::from_io_error)?;
458 StrictYamlLoader::load_from_str(&contents).map_err(|err| {
459 DotfilesError::from(
460 format!("yaml syntax error in file `{:?}`", file.as_os_str()),
461 ErrorType::YamlParseError { scan_error: err },
462 )
463 })
464}
465
466/// Process each element of the hash with the `process_function` and then fold them all using into a
467/// single result using `fold_function`, for an initial value of `init`. Returns the first error
468/// that happens in either processing or folding.
469///
470/// # Errors
471///
472/// * [ErrorType::UnexpectedYamlTypeError] if the yaml passed in is not a hash
473/// * [ErrorType::UnexpectedYamlTypeError] if the hash contains keys that cannot be parsed as
474/// strings.
475/// * Any errors from the fold_function or process function.
476pub fn fold_hash_until_first_err<T, P, Processed, F>(
477 yaml: &StrictYaml,
478 init: Result<T, DotfilesError>,
479 mut process_function: P,
480 fold_function: F,
481) -> Result<T, DotfilesError>
482where
483 P: FnMut(String, &StrictYaml) -> Result<Processed, DotfilesError>,
484 F: FnMut(T, Processed) -> Result<T, DotfilesError>,
485{
486 if let StrictYaml::Hash(hash) = yaml {
487 fold_until_first_err(
488 hash.into_iter(),
489 init,
490 |(yaml_key, yaml_value)| process_function(parse_as_string(yaml_key)?, yaml_value),
491 fold_function,
492 )
493 } else {
494 Err(DotfilesError::from_wrong_yaml(
495 "Expected StrictYaml Hash, got wrong type".to_owned(),
496 yaml.to_owned(),
497 StrictYaml::Hash(Default::default()),
498 ))
499 }
500}