Improve config loading process and tests

This commit is contained in:
Federico Terzi 2021-03-08 21:46:27 +01:00
parent 7262727823
commit 4143caff3d
5 changed files with 219 additions and 29 deletions

View File

@ -1,4 +1,6 @@
use std::collections::HashSet; use std::{collections::HashSet, path::Path};
use thiserror::Error;
use anyhow::Result;
mod path; mod path;
mod parse; mod parse;
@ -8,18 +10,36 @@ mod store;
pub trait Config { pub trait Config {
fn label(&self) -> &str; fn label(&self) -> &str;
fn match_paths(&self) -> &HashSet<String>; fn match_paths(&self) -> &[String];
fn is_match(&self, app: &AppProperties) -> bool; fn is_match(&self, app: &AppProperties) -> bool;
} }
pub trait ConfigStore<'a> { pub trait ConfigStore {
fn default(&'a self) -> &'a dyn Config; fn default(&self) -> &dyn Config;
fn active(&'a self, app: &AppProperties) -> &'a dyn Config; fn active<'a>(&'a self, app: &AppProperties) -> &'a dyn Config;
fn get_all_match_paths(&self) -> HashSet<String>;
} }
pub struct AppProperties<'a> { pub struct AppProperties<'a> {
pub title: Option<&'a str>, pub title: Option<&'a str>,
pub class: Option<&'a str>, pub class: Option<&'a str>,
pub exec: Option<&'a str>, pub exec: Option<&'a str>,
}
pub fn load_store(config_dir: &Path) -> Result<impl ConfigStore> {
store::DefaultConfigStore::load(config_dir)
}
#[derive(Error, Debug)]
pub enum ConfigStoreError {
#[error("invalid config directory")]
InvalidConfigDir(),
#[error("missing default.yml config")]
MissingDefault(),
#[error("io error")]
IOError(#[from] std::io::Error),
} }

View File

@ -15,7 +15,7 @@ pub(crate) struct ResolvedConfig {
// Generated properties // Generated properties
match_paths: HashSet<String>, match_paths: Vec<String>,
filter_title: Option<Regex>, filter_title: Option<Regex>,
filter_class: Option<Regex>, filter_class: Option<Regex>,
@ -26,7 +26,7 @@ impl Default for ResolvedConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
parsed: Default::default(), parsed: Default::default(),
match_paths: HashSet::new(), match_paths: Vec::new(),
filter_title: None, filter_title: None,
filter_class: None, filter_class: None,
filter_exec: None, filter_exec: None,
@ -39,7 +39,7 @@ impl Config for ResolvedConfig {
self.parsed.label.as_deref().unwrap_or("none") self.parsed.label.as_deref().unwrap_or("none")
} }
fn match_paths(&self) -> &HashSet<String> { fn match_paths(&self) -> &[String] {
&self.match_paths &self.match_paths
} }
@ -110,7 +110,7 @@ impl ResolvedConfig {
.parent() .parent()
.ok_or_else(ResolveError::ParentResolveFailed)?; .ok_or_else(ResolveError::ParentResolveFailed)?;
let match_paths = Self::generate_match_paths(&config, base_dir); let match_paths = Self::generate_match_paths(&config, base_dir).into_iter().collect();
let filter_title = if let Some(filter_title) = config.filter_title.as_deref() { let filter_title = if let Some(filter_title) = config.filter_title.as_deref() {
Some(Regex::new(filter_title)?) Some(Regex::new(filter_title)?)
@ -425,12 +425,17 @@ mod tests {
let config = ResolvedConfig::load(&config_file, None).unwrap(); let config = ResolvedConfig::load(&config_file, None).unwrap();
let mut expected = HashSet::new(); let mut expected = vec![
expected.insert(base_file.to_string_lossy().to_string()); base_file.to_string_lossy().to_string(),
expected.insert(another_file.to_string_lossy().to_string()); another_file.to_string_lossy().to_string(),
expected.insert(sub_file.to_string_lossy().to_string()); sub_file.to_string_lossy().to_string(),
];
expected.sort();
let mut result = config.match_paths().to_vec();
result.sort();
assert_eq!(config.match_paths(), &expected); assert_eq!(result, expected.as_slice());
}); });
} }
@ -476,16 +481,21 @@ mod tests {
let parent = ResolvedConfig::load(&parent_file, None).unwrap(); let parent = ResolvedConfig::load(&parent_file, None).unwrap();
let child = ResolvedConfig::load(&config_file, Some(&parent)).unwrap(); let child = ResolvedConfig::load(&config_file, Some(&parent)).unwrap();
let mut expected = HashSet::new(); let mut expected = vec![
expected.insert(sub_file.to_string_lossy().to_string()); sub_file.to_string_lossy().to_string(),
expected.insert(sub_under_file.to_string_lossy().to_string()); sub_under_file.to_string_lossy().to_string(),
];
expected.sort();
assert_eq!(child.match_paths(), &expected); let mut result = child.match_paths().to_vec();
result.sort();
assert_eq!(result, expected.as_slice());
let mut expected = HashSet::new(); let expected = vec![
expected.insert(base_file.to_string_lossy().to_string()); base_file.to_string_lossy().to_string()
];
assert_eq!(parent.match_paths(), &expected); assert_eq!(parent.match_paths(), expected.as_slice());
}); });
} }

View File

@ -1,16 +1,19 @@
use super::{Config, ConfigStore}; use super::{Config, ConfigStore, ConfigStoreError, resolve::ResolvedConfig};
use anyhow::Result;
use log::{debug, error};
use std::{collections::HashSet, path::Path};
pub(crate) struct DefaultConfigStore { pub(crate) struct DefaultConfigStore {
default: Box<dyn Config>, default: Box<dyn Config>,
customs: Vec<Box<dyn Config>>, customs: Vec<Box<dyn Config>>,
} }
impl<'a> ConfigStore<'a> for DefaultConfigStore { impl ConfigStore for DefaultConfigStore {
fn default(&'a self) -> &'a dyn super::Config { fn default(&self) -> &dyn super::Config {
self.default.as_ref() self.default.as_ref()
} }
fn active(&'a self, app: &super::AppProperties) -> &'a dyn super::Config { fn active<'a>(&'a self, app: &super::AppProperties) -> &'a dyn super::Config {
// Find a custom config that matches or fallback to the default one // Find a custom config that matches or fallback to the default one
for custom in self.customs.iter() { for custom in self.customs.iter() {
if custom.is_match(app) { if custom.is_match(app) {
@ -19,6 +22,64 @@ impl<'a> ConfigStore<'a> for DefaultConfigStore {
} }
self.default.as_ref() self.default.as_ref()
} }
// TODO: test
fn get_all_match_paths(&self) -> HashSet<String> {
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 {
// TODO: test
pub fn load(config_dir: &Path) -> Result<Self> {
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 default = ResolvedConfig::load(&default_file, None)?;
debug!("loaded default config at path: {:?}", default_file);
// Then the others
let mut customs: Vec<Box<dyn Config>> = 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(Box::new(config));
debug!("loaded config at path: {:?}", config_file);
}
Err(err) => {
error!("unable to load config at path: {:?}, with error: {}", config_file, err);
}
}
}
}
Ok(Self {
default: Box::new(default),
customs,
})
}
} }
#[cfg(test)] #[cfg(test)]
@ -44,7 +105,7 @@ mod tests {
&self.label &self.label
} }
fn match_paths(&self) -> &std::collections::HashSet<String> { fn match_paths(&self) -> &[String] {
unimplemented!() unimplemented!()
} }

View File

@ -16,10 +16,109 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::path::Path;
use config::ConfigStore;
use matches::store::MatchStore;
use anyhow::Result;
use thiserror::Error;
#[macro_use] #[macro_use]
extern crate lazy_static; extern crate lazy_static;
mod util; mod util;
mod config;
mod matches;
mod counter; mod counter;
pub mod config;
pub mod matches;
pub fn load(base_path: &Path) -> Result<(impl ConfigStore, impl MatchStore)> {
let config_dir = base_path.join("config");
if !config_dir.exists() || !config_dir.is_dir() {
return Err(ConfigError::MissingConfigDir().into())
}
let config_store = config::load_store(&config_dir)?;
let root_paths = config_store.get_all_match_paths();
let mut match_store = matches::store::new();
match_store.load(&root_paths.into_iter().collect::<Vec<String>>());
Ok((config_store, match_store))
}
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("missing config directory")]
MissingConfigDir(),
}
#[cfg(test)]
mod tests {
use super::*;
use config::{AppProperties, ConfigStore};
use crate::util::tests::use_test_directory;
#[test]
fn load_works_correctly() {
use_test_directory(|base, match_dir, config_dir| {
let base_file = match_dir.join("base.yml");
std::fs::write(&base_file, r#"
matches:
- trigger: "hello"
replace: "world"
"#).unwrap();
let another_file = match_dir.join("another.yml");
std::fs::write(&another_file, r#"
imports:
- "_sub.yml"
matches:
- trigger: "hello2"
replace: "world2"
"#).unwrap();
let under_file = match_dir.join("_sub.yml");
std::fs::write(&under_file, r#"
matches:
- trigger: "hello3"
replace: "world3"
"#).unwrap();
let config_file = config_dir.join("default.yml");
std::fs::write(&config_file, "").unwrap();
let custom_config_file = config_dir.join("custom.yml");
std::fs::write(&custom_config_file, r#"
filter_title: "Chrome"
use_standard_includes: false
includes: ["../match/another.yml"]
"#).unwrap();
let (config_store, match_store) = load(&base).unwrap();
assert_eq!(config_store.default().match_paths().len(), 2);
assert_eq!(config_store.active(&AppProperties {
title: Some("Google Chrome"),
class: None,
exec: None,
}).match_paths().len(), 1);
assert_eq!(match_store.query(config_store.default().match_paths()).matches.len(), 3);
assert_eq!(match_store.query(config_store.active(&AppProperties {
title: Some("Chrome"),
class: None,
exec: None,
}).match_paths()).matches.len(), 2);
});
}
#[test]
fn load_without_valid_config_dir() {
use_test_directory(|_, match_dir, _| {
// To correcly load the configs, the "load" method looks for the "config" directory
assert!(load(&match_dir).is_err());
});
}
}

View File

@ -3,7 +3,7 @@ use serde_yaml::Mapping;
use crate::counter::{next_id, StructId}; use crate::counter::{next_id, StructId};
mod group; mod group;
mod store; pub mod store;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Match { pub struct Match {