libscilo/config/
parse.rs

1//! Helper functions to parse a configuration file written by a user to create
2//! [`ConfigFile`s][crate::config::file::ConfigFile] and [`InstantiatedConfig`s][crate::config::instantiated_config::InstantiatedConfig].
3
4use log::info;
5use std::{
6    fs::File,
7    io::{self, Read},
8    path::Path,
9};
10
11use super::{
12    error::ConfigError, file::ConfigFile, find_config_paths,
13    instantiated_config::InstantiatedConfig,
14};
15
16/// A helper function to dump an entire file's contents into memory.
17///
18/// Not to be used for files of unknown sizes that can eat up too much memory.
19pub(crate) fn file_to_string(path: &Path) -> io::Result<String> {
20    // open the file for reading
21    let mut file = File::open(path)?;
22
23    // read the contents
24    let mut file_contents = String::new();
25    file.read_to_string(&mut file_contents)?;
26
27    Ok(file_contents)
28}
29
30/// Parse the configuration settings from a given configuration file.
31pub(crate) fn parse_config_file(path: &Path) -> Result<ConfigFile, ConfigError> {
32    info!("Parsing configuration file: {}", path.display());
33    // check that the path exists
34    if !path.exists() {
35        return Err(ConfigError::DoesNotExist(path.to_path_buf()));
36    } else if !path.is_file() {
37        return Err(ConfigError::NotAFile(path.to_path_buf()));
38    }
39
40    // try reading the file and parse its contents
41    match file_to_string(path) {
42        Ok(file_contents) => {
43            // try to deserialize the config file directly
44            let cfg: ConfigFile = match toml::from_str(&file_contents) {
45                Ok(cfg) => cfg,
46                Err(e) => {
47                    return Err(ConfigError::DeserializationError(
48                        path.to_path_buf(),
49                        e.message().to_string(),
50                    ))
51                }
52            };
53
54            Ok(cfg)
55        }
56        Err(e) => Err(ConfigError::IoError(path.to_path_buf(), e.kind())),
57    }
58}
59
60/// Find and parse all relevant configuration files for the current invocation.
61///
62/// Multiple configuration files can be detected and parsed.
63/// See [`find_config_paths()`][crate::config::find_config_paths] for the order in which configuration files are found and applied.
64/// Configurations specified in a directory will apply to that directory's contents and all its subdirectories.
65pub fn parse_all_config_files() -> Result<InstantiatedConfig, ConfigError> {
66    // This complicated iteration and mapping will automatically return a `ConfigError`
67    // if there is a typo or other misspecification in the configuration file.
68    let cfg_files: Vec<ConfigFile> = find_config_paths()
69        .iter()
70        .map(|path| parse_config_file(path))
71        .collect::<Result<Vec<_>, ConfigError>>()?;
72
73    if !cfg_files.is_empty() {
74        let merged_config_file = merge_configs(cfg_files);
75
76        // take the merged set of configurations
77        InstantiatedConfig::try_from(merged_config_file)
78    } else {
79        Ok(InstantiatedConfig::default())
80    }
81}
82
83/// Merge separate [`InstantiatedConfig`] objects together in a hierarchical way.
84///
85/// The supplied `cfgs` is assumed to have the configurations listed in order of
86/// priority, with the highest priority element first.
87/// Options listed in configurations at the end of the list will be overruled if
88/// those same options are `Some()` in earlier configurations.
89fn merge_configs(cfgs: Vec<ConfigFile>) -> ConfigFile {
90    // for testing purposes, just return the first config
91    cfgs[0].clone()
92}