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