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 parse;
@ -8,14 +10,16 @@ mod store;
pub trait Config {
fn label(&self) -> &str;
fn match_paths(&self) -> &HashSet<String>;
fn match_paths(&self) -> &[String];
fn is_match(&self, app: &AppProperties) -> bool;
}
pub trait ConfigStore<'a> {
fn default(&'a self) -> &'a dyn Config;
fn active(&'a self, app: &AppProperties) -> &'a dyn Config;
pub trait ConfigStore {
fn default(&self) -> &dyn Config;
fn active<'a>(&'a self, app: &AppProperties) -> &'a dyn Config;
fn get_all_match_paths(&self) -> HashSet<String>;
}
pub struct AppProperties<'a> {
@ -23,3 +27,19 @@ pub struct AppProperties<'a> {
pub class: 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
match_paths: HashSet<String>,
match_paths: Vec<String>,
filter_title: Option<Regex>,
filter_class: Option<Regex>,
@ -26,7 +26,7 @@ impl Default for ResolvedConfig {
fn default() -> Self {
Self {
parsed: Default::default(),
match_paths: HashSet::new(),
match_paths: Vec::new(),
filter_title: None,
filter_class: None,
filter_exec: None,
@ -39,7 +39,7 @@ impl Config for ResolvedConfig {
self.parsed.label.as_deref().unwrap_or("none")
}
fn match_paths(&self) -> &HashSet<String> {
fn match_paths(&self) -> &[String] {
&self.match_paths
}
@ -110,7 +110,7 @@ impl ResolvedConfig {
.parent()
.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() {
Some(Regex::new(filter_title)?)
@ -425,12 +425,17 @@ mod tests {
let config = ResolvedConfig::load(&config_file, None).unwrap();
let mut expected = HashSet::new();
expected.insert(base_file.to_string_lossy().to_string());
expected.insert(another_file.to_string_lossy().to_string());
expected.insert(sub_file.to_string_lossy().to_string());
let mut expected = vec![
base_file.to_string_lossy().to_string(),
another_file.to_string_lossy().to_string(),
sub_file.to_string_lossy().to_string(),
];
expected.sort();
assert_eq!(config.match_paths(), &expected);
let mut result = config.match_paths().to_vec();
result.sort();
assert_eq!(result, expected.as_slice());
});
}
@ -476,16 +481,21 @@ mod tests {
let parent = ResolvedConfig::load(&parent_file, None).unwrap();
let child = ResolvedConfig::load(&config_file, Some(&parent)).unwrap();
let mut expected = HashSet::new();
expected.insert(sub_file.to_string_lossy().to_string());
expected.insert(sub_under_file.to_string_lossy().to_string());
let mut expected = vec![
sub_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();
expected.insert(base_file.to_string_lossy().to_string());
let expected = vec![
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 {
default: Box<dyn Config>,
customs: Vec<Box<dyn Config>>,
}
impl<'a> ConfigStore<'a> for DefaultConfigStore {
fn default(&'a self) -> &'a dyn super::Config {
impl ConfigStore for DefaultConfigStore {
fn default(&self) -> &dyn super::Config {
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
for custom in self.customs.iter() {
if custom.is_match(app) {
@ -19,6 +22,64 @@ impl<'a> ConfigStore<'a> for DefaultConfigStore {
}
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)]
@ -44,7 +105,7 @@ mod tests {
&self.label
}
fn match_paths(&self) -> &std::collections::HashSet<String> {
fn match_paths(&self) -> &[String] {
unimplemented!()
}

View File

@ -16,10 +16,109 @@
* You should have received a copy of the GNU General Public License
* 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]
extern crate lazy_static;
mod util;
mod config;
mod matches;
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};
mod group;
mod store;
pub mod store;
#[derive(Debug, Clone)]
pub struct Match {