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}