/* * This file is part of espanso. * * Copyright (C) 2019-2021 Federico Terzi * * espanso is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * espanso is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with espanso. If not, see . */ use crate::error::NonFatalErrorSet; use super::{resolve::ResolvedConfig, Config, ConfigStore, ConfigStoreError}; use anyhow::{Context, Result}; use log::{debug, error}; use std::sync::Arc; use std::{collections::HashSet, path::Path}; pub(crate) struct DefaultConfigStore { default: Arc, customs: Vec>, } impl ConfigStore for DefaultConfigStore { fn default(&self) -> Arc { Arc::clone(&self.default) } fn active<'a>(&'a self, app: &super::AppProperties) -> Arc { // Find a custom config that matches or fallback to the default one for custom in self.customs.iter() { if custom.is_match(app) { return Arc::clone(custom); } } Arc::clone(&self.default) } fn configs(&self) -> Vec> { let mut configs = vec![Arc::clone(&self.default)]; for custom in self.customs.iter() { configs.push(Arc::clone(custom)); } configs } // TODO: test fn get_all_match_paths(&self) -> HashSet { let mut paths = HashSet::new(); paths.extend(self.default().match_paths().iter().cloned()); for custom in self.customs.iter() { paths.extend(custom.match_paths().iter().cloned()); } paths } } impl DefaultConfigStore { pub fn load(config_dir: &Path) -> Result<(Self, Vec)> { if !config_dir.is_dir() { return Err(ConfigStoreError::InvalidConfigDir().into()); } // First get the default.yml file let default_file = config_dir.join("default.yml"); if !default_file.exists() || !default_file.is_file() { return Err(ConfigStoreError::MissingDefault().into()); } let mut non_fatal_errors = Vec::new(); let default = ResolvedConfig::load(&default_file, None) .context("failed to load default.yml configuration")?; debug!("loaded default config at path: {:?}", default_file); // Then the others let mut customs: Vec> = Vec::new(); for entry in std::fs::read_dir(config_dir).map_err(ConfigStoreError::IOError)? { let entry = entry?; let config_file = entry.path(); let extension = config_file .extension() .unwrap_or_default() .to_string_lossy() .to_lowercase(); // Additional config files are loaded best-effort if config_file.is_file() && config_file != default_file && (extension == "yml" || extension == "yaml") { match ResolvedConfig::load(&config_file, Some(&default)) { Ok(config) => { customs.push(Arc::new(config)); debug!("loaded config at path: {:?}", config_file); } Err(err) => { error!( "unable to load config at path: {:?}, with error: {}", config_file, err ); non_fatal_errors.push(NonFatalErrorSet::single_error(&config_file, err)); } } } } Ok(( Self { default: Arc::new(default), customs, }, non_fatal_errors, )) } pub fn from_configs(default: Arc, customs: Vec>) -> Result { Ok(Self { default, customs }) } } #[cfg(test)] mod tests { use super::*; use crate::config::MockConfig; pub fn new_mock(label: &'static str, is_match: bool) -> MockConfig { let label = label.to_owned(); let mut mock = MockConfig::new(); mock.expect_id().return_const(0); mock.expect_label().return_const(label); mock.expect_is_match().return_const(is_match); mock } #[test] fn config_store_selects_correctly() { let default = new_mock("default", false); let custom1 = new_mock("custom1", false); let custom2 = new_mock("custom2", true); let store = DefaultConfigStore { default: Arc::new(default), customs: vec![Arc::new(custom1), Arc::new(custom2)], }; assert_eq!(store.default().label(), "default"); assert_eq!( store .active(&crate::config::AppProperties { title: None, class: None, exec: None, }) .label(), "custom2" ); } #[test] fn config_store_active_fallback_to_default_if_no_match() { let default = new_mock("default", false); let custom1 = new_mock("custom1", false); let custom2 = new_mock("custom2", false); let store = DefaultConfigStore { default: Arc::new(default), customs: vec![Arc::new(custom1), Arc::new(custom2)], }; assert_eq!(store.default().label(), "default"); assert_eq!( store .active(&crate::config::AppProperties { title: None, class: None, exec: None, }) .label(), "default" ); } }