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}