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}