use crate::{ConfigError, Result};
use cfgmatic_files::{ConfigFile, ConfigFiles, FileFinder, MergeOptions};
use cfgmatic_paths::{ConfigFileRule, ConfigRuleSet, PathsBuilder};
use serde::de::DeserializeOwned;
#[derive(Debug, Clone)]
pub struct MultiFileConfigLoader {
pub app_name: String,
pub rules: Option<ConfigRuleSet>,
pub merge_options: MergeOptions,
}
impl MultiFileConfigLoader {
#[must_use]
pub fn new(app_name: impl Into<String>) -> Self {
Self {
app_name: app_name.into(),
rules: None,
merge_options: MergeOptions::new(),
}
}
#[must_use]
pub fn rules(mut self, rules: ConfigRuleSet) -> Self {
self.rules = Some(rules);
self
}
#[must_use]
pub const fn merge_options(mut self, options: MergeOptions) -> Self {
self.merge_options = options;
self
}
pub fn load<T>(&self) -> Result<T>
where
T: DeserializeOwned + Default,
{
let path_finder = PathsBuilder::new(&self.app_name).build();
if let Some(rules) = &self.rules {
let mut files = ConfigFiles::new();
let mut has_required = false;
for rule in &rules.main_files {
let found = self.find_files_by_rule(&path_finder, rule)?;
if !found.is_empty() {
has_required = true;
}
for file in found {
files.push(file);
}
}
let has_any_required = rules.main_files.iter().any(|r| r.required);
if has_any_required && !has_required && files.is_empty() {
return Err(ConfigError::NotFound(format!(
"No configuration files found for '{}'",
self.app_name
)));
}
self.merge_files(files)
} else {
let finder = FileFinder::new(&self.app_name).build();
let files = finder.find()?;
self.merge_files(files)
}
}
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
fn find_files_by_rule(
&self,
finder: &cfgmatic_paths::PathFinder,
rule: &ConfigFileRule,
) -> Result<Vec<ConfigFile>> {
let mut found = Vec::new();
for tier in rule.tiers.tiers() {
let dirs = match tier {
cfgmatic_paths::ConfigTier::User => finder.user_dirs(),
cfgmatic_paths::ConfigTier::Local => finder.local_dirs(),
cfgmatic_paths::ConfigTier::System => finder.system_dirs(),
};
for dir in dirs {
if let Ok(entries) = std::fs::read_dir(&dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_file()
&& rule.pattern.matches(&path)
&& let Some(format) = cfgmatic_files::Format::from_path(&path)
{
found.push(ConfigFile {
path,
tier,
format,
content: None,
});
}
}
}
}
}
Ok(found)
}
fn merge_files<T>(&self, files: ConfigFiles) -> Result<T>
where
T: DeserializeOwned + Default,
{
if files.is_empty() {
return Ok(T::default());
}
let mut result = T::default();
for mut file in files {
let value: T = file.parse().map_err(|e| {
ConfigError::Parse(format!("Failed to parse '{}': {}", file.path.display(), e))
})?;
result = self.merge_values(result, value)?;
}
Ok(result)
}
#[allow(clippy::unused_self, clippy::unnecessary_wraps)]
fn merge_values<T>(&self, _base: T, other: T) -> Result<T>
where
T: DeserializeOwned + Default,
{
Ok(other)
}
}
pub fn load_multi<T>(app_name: impl Into<String>) -> Result<T>
where
T: DeserializeOwned + Default,
{
MultiFileConfigLoader::new(app_name).load()
}